graphics: add custom shaders (water, sky, sunrays, post-processing) and outdoor lighting
This commit is contained in:
parent
04599db6d6
commit
33e326306a
|
|
@ -151,7 +151,6 @@ class GZHoodDataAI(HoodDataAI.HoodDataAI):
|
|||
self.golfKarts += foundKarts
|
||||
self.golfKartGroups += foundKartGroups
|
||||
|
||||
print(self.golfKarts, self.golfKartGroups)
|
||||
for golfKart in self.golfKarts:
|
||||
golfKart.start()
|
||||
self.addDistObj(golfKart)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from toontown.toon.Toon import teleportDebug
|
|||
from toontown.toonbase import ToontownGlobals
|
||||
from toontown.toonbase import TTLocalizer
|
||||
from toontown.toonbase.ToonBaseGlobal import base
|
||||
from direct.showbase.ShowBaseGlobal import hidden
|
||||
|
||||
|
||||
class Hood(StateData):
|
||||
|
|
@ -84,6 +85,44 @@ class Hood(StateData):
|
|||
|
||||
base.localAvatar.stopChat()
|
||||
|
||||
def _loadSkyModel(self, path, halloween=False):
|
||||
"""Load a sky model; if the file is missing use a placeholder.
|
||||
|
||||
Legacy sky BAM files are removed when ProceduralSky is active, so a
|
||||
missing file is the normal case and logged at debug level only.
|
||||
"""
|
||||
# When ProceduralSky is active the old sky BAM files are intentionally
|
||||
# deleted – don't try to load them at all.
|
||||
try:
|
||||
from toontown.hood.SkyUtil import _wantProceduralSky
|
||||
if _wantProceduralSky():
|
||||
sky = NodePath('legacySkyDisabled')
|
||||
sky.reparentTo(hidden)
|
||||
sky.setTag('sky', 'Halloween' if halloween else 'Regular')
|
||||
return sky
|
||||
except Exception:
|
||||
pass
|
||||
sky = base.loader.loadModel(path)
|
||||
try:
|
||||
valid = sky is not None and not sky.isEmpty()
|
||||
except Exception:
|
||||
valid = False
|
||||
if not valid:
|
||||
self.notify.debug('Sky model not found (%r) – ProceduralSky will be used.' % path)
|
||||
sky = NodePath('missingSkyPlaceholder')
|
||||
sky.reparentTo(hidden)
|
||||
sky.setTag('sky', 'Halloween' if halloween else 'Regular')
|
||||
return sky
|
||||
sky.setTag('sky', 'Halloween' if halloween else 'Regular')
|
||||
sky.setScale(1.0)
|
||||
if not halloween:
|
||||
sky.setFogOff()
|
||||
try:
|
||||
sky.flattenLight()
|
||||
except Exception:
|
||||
pass
|
||||
return sky
|
||||
|
||||
def load(self):
|
||||
if self.storageDNAFile:
|
||||
base.loader.loadDNAFile(self.dnaStore, self.storageDNAFile)
|
||||
|
|
@ -96,23 +135,12 @@ class Hood(StateData):
|
|||
base.loader.loadDNAFile(self.dnaStore, storageFile)
|
||||
|
||||
if ToontownGlobals.HALLOWEEN_COSTUMES not in holidayIds and ToontownGlobals.SPOOKY_COSTUMES not in holidayIds or not self.spookySkyFile:
|
||||
self.sky = base.loader.loadModel(self.skyFile)
|
||||
self.sky.setTag('sky', 'Regular')
|
||||
self.sky.setScale(1.0)
|
||||
self.sky.setFogOff()
|
||||
# Flatten sky for better performance
|
||||
self.sky.flattenLight()
|
||||
self.sky = self._loadSkyModel(self.skyFile, halloween=False)
|
||||
else:
|
||||
self.sky = base.loader.loadModel(self.spookySkyFile)
|
||||
self.sky.setTag('sky', 'Halloween')
|
||||
self.sky.flattenLight()
|
||||
self.sky = self._loadSkyModel(self.spookySkyFile, halloween=True)
|
||||
|
||||
if not newsManager:
|
||||
self.sky = base.loader.loadModel(self.skyFile)
|
||||
self.sky.setTag('sky', 'Regular')
|
||||
self.sky.setScale(1.0)
|
||||
self.sky.setFogOff()
|
||||
self.sky.flattenLight()
|
||||
self.sky = self._loadSkyModel(self.skyFile, halloween=False)
|
||||
|
||||
def unload(self):
|
||||
if hasattr(self, 'loader'):
|
||||
|
|
@ -241,6 +269,25 @@ class Hood(StateData):
|
|||
messenger.send(self.doneEvent)
|
||||
|
||||
def startSky(self):
|
||||
# If the procedural sky shader system is active, suppress the legacy
|
||||
# model sky entirely so it can never render over the shader dome.
|
||||
try:
|
||||
from toontown.hood.SkyUtil import _wantProceduralSky
|
||||
if _wantProceduralSky():
|
||||
try:
|
||||
if self.sky and not self.sky.isEmpty():
|
||||
self.sky.removeNode()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from direct.showbase.ShowBaseGlobal import hidden
|
||||
self.sky = NodePath('legacySkyDisabled')
|
||||
self.sky.reparentTo(hidden)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self.sky.reparentTo(base.camera)
|
||||
self.sky.setZ(0.0)
|
||||
self.sky.setHpr(0.0, 0.0, 0.0)
|
||||
|
|
@ -258,8 +305,14 @@ class Hood(StateData):
|
|||
if hasattr(self, 'sky') and self.sky:
|
||||
self.stopSky()
|
||||
|
||||
self.sky = base.loader.loadModel(self.spookySkyFile)
|
||||
self.sky.setTag('sky', 'Halloween')
|
||||
# Uses _loadSkyModel so missing/deleted BAMs (replaced by ProceduralSky) do not crash.
|
||||
self.sky = self._loadSkyModel(self.spookySkyFile, halloween=True)
|
||||
try:
|
||||
nm = self.sky.getName()
|
||||
if nm in ('legacySkyDisabled', 'missingSkyPlaceholder'):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self.sky.setColor(0.5, 0.5, 0.5, 1)
|
||||
self.sky.reparentTo(base.camera)
|
||||
self.sky.setTransparency(TransparencyAttrib.MDual, 1)
|
||||
|
|
@ -272,10 +325,12 @@ class Hood(StateData):
|
|||
|
||||
def endSpookySky(self):
|
||||
if hasattr(self, 'sky') and self.sky:
|
||||
self.sky.reparentTo(base.hidden)
|
||||
try:
|
||||
self.sky.reparentTo(base.hidden)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if hasattr(self, 'sky'):
|
||||
self.sky = base.loader.loadModel(self.skyFile)
|
||||
self.sky.setTag('sky', 'Regular')
|
||||
self.sky.setScale(1.0)
|
||||
self.startSky()
|
||||
# Never call loader.loadModel(self.skyFile) here — legacy TT_sky.bam et al. may be
|
||||
# removed when ProceduralSky is enabled; _loadSkyModel returns a safe placeholder.
|
||||
self.sky = self._loadSkyModel(self.skyFile, halloween=False)
|
||||
self.startSky()
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ class InteractiveAnimatedProp(GenericAnimatedProp.GenericAnimatedProp, FSM.FSM):
|
|||
GenericAnimatedProp.GenericAnimatedProp.__init__(self, node)
|
||||
return
|
||||
|
||||
def _isHolidayRunning(self):
|
||||
newsMgr = getattr(getattr(base, 'cr', None), 'newsManager', None)
|
||||
if not newsMgr:
|
||||
return False
|
||||
return newsMgr.isHolidayRunning(self.holidayId)
|
||||
|
||||
def delete(self):
|
||||
self.exit()
|
||||
GenericAnimatedProp.GenericAnimatedProp.delete(self)
|
||||
|
|
@ -202,7 +208,7 @@ class InteractiveAnimatedProp(GenericAnimatedProp.GenericAnimatedProp, FSM.FSM):
|
|||
GenericAnimatedProp.GenericAnimatedProp.enter(self)
|
||||
if base.config.GetBool('props-buff-battles', True):
|
||||
self.notify.debug('props buff battles is true')
|
||||
if base.cr.newsManager.isHolidayRunning(self.holidayId):
|
||||
if self._isHolidayRunning():
|
||||
self.notify.debug('holiday is running, doing idle interval')
|
||||
self.node.stop()
|
||||
self.node.pose('idle0', 0)
|
||||
|
|
@ -343,28 +349,28 @@ class InteractiveAnimatedProp(GenericAnimatedProp.GenericAnimatedProp, FSM.FSM):
|
|||
|
||||
def gotoFaceoff(self):
|
||||
self.notify.debugStateCall(self)
|
||||
if base.cr.newsManager.isHolidayRunning(self.holidayId):
|
||||
if self._isHolidayRunning():
|
||||
self.request('Faceoff')
|
||||
else:
|
||||
self.notify.debug('not going to faceoff because holiday %d is not running' % self.holidayId)
|
||||
|
||||
def gotoBattleCheer(self):
|
||||
self.notify.debugStateCall(self)
|
||||
if base.cr.newsManager.isHolidayRunning(self.holidayId):
|
||||
if self._isHolidayRunning():
|
||||
self.request('BattleCheer')
|
||||
else:
|
||||
self.notify.debug('not going to battleCheer because holiday %d is not running' % self.holidayId)
|
||||
|
||||
def gotoIdle(self):
|
||||
self.notify.debugStateCall(self)
|
||||
if base.cr.newsManager.isHolidayRunning(self.holidayId):
|
||||
if self._isHolidayRunning():
|
||||
self.request('DoIdleAnim')
|
||||
else:
|
||||
self.notify.debug('not going to idle because holiday %d is not running' % self.holidayId)
|
||||
|
||||
def gotoVictory(self):
|
||||
self.notify.debugStateCall(self)
|
||||
if base.cr.newsManager.isHolidayRunning(self.holidayId):
|
||||
if self._isHolidayRunning():
|
||||
self.request('Victory')
|
||||
else:
|
||||
self.notify.debug('not going to victory because holiday %d is not running' % self.holidayId)
|
||||
|
|
@ -372,7 +378,7 @@ class InteractiveAnimatedProp(GenericAnimatedProp.GenericAnimatedProp, FSM.FSM):
|
|||
def gotoSad(self, buildingDoId):
|
||||
self.notify.debugStateCall(self)
|
||||
self.buildingsMakingMeSad.add(buildingDoId)
|
||||
if base.cr.newsManager.isHolidayRunning(self.holidayId):
|
||||
if self._isHolidayRunning():
|
||||
self.request('Sad')
|
||||
else:
|
||||
self.notify.debug('not going to sad because holiday %d is not running' % self.holidayId)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -774,6 +774,8 @@ class Place(StateData, FriendsListManager):
|
|||
base.localAvatar.stopPosHprBroadcast()
|
||||
|
||||
def requestTeleport(self, hoodId, zoneId, shardId, avId):
|
||||
if avId is None:
|
||||
avId = -1
|
||||
if avId > 0:
|
||||
teleportNotify.debug('requestTeleport%s' % ((hoodId,
|
||||
zoneId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
"""Procedural code-generated sky system for Toontown.
|
||||
|
||||
Replaces the hood.sky model-based sky with a GLSL-driven atmospheric sky dome
|
||||
that includes:
|
||||
• Rayleigh + Mie scattering sky gradient
|
||||
• Wide sunset/sunrise horizon corona
|
||||
• Visible SUN DISC with corona, limb darkening, blinding glare
|
||||
• Volumetric-looking FBM cumulus clouds with domain warping
|
||||
• Twinkling star field with spectral colour variation
|
||||
• Moon disc with surface detail and corona (all night zones)
|
||||
• Full day/night cycle integration
|
||||
|
||||
Usage (from SkyUtil / OutdoorLighting)
|
||||
──────────────────────────────────────
|
||||
from toontown.hood.ProceduralSky import ProceduralSky
|
||||
|
||||
sky = ProceduralSky()
|
||||
# Prefer the lens NodePath (e.g. base.cam) so the dome matches the view matrix
|
||||
# used for rendering; base.camera is fine when it coincides with the lens.
|
||||
sky.attach(base.cam)
|
||||
sky.update(spec, timeOfDay) # call each frame (or at least on spec changes)
|
||||
sky.detach() # cleanup on zone exit
|
||||
|
||||
The ProceduralSky.update() signature accepts the same 'spec' dict as
|
||||
OutdoorLighting zone profiles, so integration is zero-cost.
|
||||
|
||||
Per-zone sky parameters (added to _ZONE_PROFILES by OutdoorLighting):
|
||||
cloudCoverage – 0.0–1.0
|
||||
cloudSpeed – relative speed multiplier
|
||||
cloudSharpness – 0.0 (soft) – 1.0 (sharp)
|
||||
turbidity – 1.0–8.0 (Mie haze)
|
||||
starBrightness – 0.0–1.0
|
||||
moonEnabled – bool (all night zones, not just DL)
|
||||
moonDir – (h,p,r) HPR for moon direction (matches keyHpr format)
|
||||
skyExposure – scalar (passed to post-process; default 1.0)
|
||||
sunBlindStrength– 0.0–1.0 blinding glare when looking at sun (default 0.85)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import os
|
||||
|
||||
from panda3d.core import (
|
||||
Geom,
|
||||
GeomNode,
|
||||
GeomTriangles,
|
||||
GeomVertexData,
|
||||
GeomVertexFormat,
|
||||
GeomVertexWriter,
|
||||
Filename,
|
||||
NodePath,
|
||||
Shader,
|
||||
Vec3,
|
||||
Vec4,
|
||||
)
|
||||
|
||||
from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
|
||||
from toontown.toonbase.ToonBaseGlobal import base
|
||||
|
||||
_SHADER_DIR = os.path.join(os.path.dirname(__file__), '..', 'shaders')
|
||||
_SKY_VERT = os.path.join(_SHADER_DIR, 'sky.vert.glsl')
|
||||
_SKY_FRAG = os.path.join(_SHADER_DIR, 'sky.frag.glsl')
|
||||
|
||||
_SKY_RADIUS = 950.0 # units – large enough to contain all Toontown geometry
|
||||
_CLOUD_BASE_SPEED = 0.60 # base cloud animation speed (modified per zone)
|
||||
|
||||
_sky_shader: Shader | None = None
|
||||
|
||||
|
||||
def _loadSkyShader() -> Shader | None:
|
||||
global _sky_shader
|
||||
if _sky_shader is not None:
|
||||
return _sky_shader
|
||||
try:
|
||||
from toontown.hood import OutdoorLighting as osl
|
||||
if getattr(osl, '_OUTDOOR_SHADER_BISECT_LEVEL', 0) < 1:
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
vp_os = os.path.normpath(_SKY_VERT)
|
||||
fp_os = os.path.normpath(_SKY_FRAG)
|
||||
if not (os.path.isfile(vp_os) and os.path.isfile(fp_os)):
|
||||
return None
|
||||
# On Windows, Panda3D's shader loader expects Panda-style paths (eg
|
||||
# `/c/Users/...`) rather than raw OS paths with backslashes.
|
||||
vp = Filename.fromOsSpecific(vp_os)
|
||||
fp = Filename.fromOsSpecific(fp_os)
|
||||
try:
|
||||
vp.makeTrueCase()
|
||||
fp.makeTrueCase()
|
||||
except Exception:
|
||||
pass
|
||||
_sky_shader = Shader.load(Shader.SL_GLSL, vp, fp)
|
||||
return _sky_shader
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _makeSkySphereMesh(radius: float = _SKY_RADIUS,
|
||||
latSegs: int = 18,
|
||||
lonSegs: int = 36) -> NodePath:
|
||||
"""Build an inward-facing UV sphere NodePath.
|
||||
|
||||
The sphere is centred at the origin in model space. When attached to the
|
||||
camera NodePath the camera is always at the sphere centre, so every vertex
|
||||
position is a ray direction from the camera.
|
||||
"""
|
||||
vfmt = GeomVertexFormat.getV3()
|
||||
vdata = GeomVertexData('skyDomeMesh', vfmt, Geom.UHStatic)
|
||||
vdata.setNumRows((latSegs + 1) * (lonSegs + 1))
|
||||
vwrite = GeomVertexWriter(vdata, 'vertex')
|
||||
|
||||
for lat in range(latSegs + 1):
|
||||
phi = math.pi * lat / latSegs # 0 → π (north pole → south pole)
|
||||
sp = math.sin(phi)
|
||||
cp = math.cos(phi)
|
||||
for lon in range(lonSegs + 1):
|
||||
theta = 2.0 * math.pi * lon / lonSegs
|
||||
x = radius * sp * math.cos(theta)
|
||||
y = radius * sp * math.sin(theta)
|
||||
z = radius * cp
|
||||
vwrite.addData3(x, y, z)
|
||||
|
||||
tris = GeomTriangles(Geom.UHStatic)
|
||||
stride = lonSegs + 1
|
||||
for lat in range(latSegs):
|
||||
for lon in range(lonSegs):
|
||||
v0 = lat * stride + lon
|
||||
v1 = lat * stride + lon + 1
|
||||
v2 = (lat + 1) * stride + lon
|
||||
v3 = (lat + 1) * stride + lon + 1
|
||||
# Inward-facing: flip winding compared to outward sphere.
|
||||
tris.addVertices(v0, v2, v1)
|
||||
tris.addVertices(v1, v2, v3)
|
||||
tris.closePrimitive()
|
||||
|
||||
geom = Geom(vdata)
|
||||
geom.addPrimitive(tris)
|
||||
gnode = GeomNode('skyDomeGeom')
|
||||
gnode.addGeom(geom)
|
||||
return NodePath(gnode)
|
||||
|
||||
|
||||
def _dir_world_to_cam(world_dir: Vec3) -> Vec3:
|
||||
"""Map a world-space *direction* into the active camera's local space.
|
||||
|
||||
The sky dome is parented to ``base.cam``, so ``vDir`` in the GLSL fragment
|
||||
shader is in **camera space**. ``sunDir`` / ``moonDir`` must match that
|
||||
space or dot products (sun disc, clouds, Mie) are wrong and the sky looks
|
||||
like a flat clear colour with no sun or clouds.
|
||||
"""
|
||||
try:
|
||||
cam = getattr(base, 'cam', None)
|
||||
rnp = getattr(base, 'render', None)
|
||||
if cam is None or rnp is None or cam.isEmpty() or rnp.isEmpty():
|
||||
return Vec3(world_dir)
|
||||
v = cam.getRelativeVector(rnp, Vec3(world_dir))
|
||||
ln = v.length()
|
||||
if ln > 1.0e-7:
|
||||
v /= ln
|
||||
return v
|
||||
except Exception:
|
||||
return Vec3(world_dir)
|
||||
|
||||
|
||||
def _hprToDir(h_deg: float, p_deg: float) -> Vec3:
|
||||
"""Convert a Panda3D HPR heading/pitch to a world-space direction vector.
|
||||
|
||||
The direction returned is the *forward* vector that a node with (H, P, 0)
|
||||
orientation points toward. For the sun key light this is the direction the
|
||||
light shines (scene ← sun); for sun position in the sky pass the negated
|
||||
result.
|
||||
"""
|
||||
h = math.radians(h_deg)
|
||||
p = math.radians(p_deg)
|
||||
# Panda3D right-hand Y-forward Z-up:
|
||||
# H rotates around Z (clockwise from above, i.e. left-hand around Z)
|
||||
# P rotates around X after H
|
||||
x = math.sin(h) * math.cos(p)
|
||||
y = -math.cos(h) * math.cos(p)
|
||||
z = -math.sin(p)
|
||||
return Vec3(x, y, z)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Per-zone cloud + sky parameters defaults
|
||||
# (OutdoorLighting zones may override any of these in their profile dict)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_ZONE_SKY_DEFAULTS: dict[str, dict] = {
|
||||
'tt': {'cloudCoverage': 0.42, 'cloudSpeed': 0.70, 'cloudSharpness': 0.55,
|
||||
'turbidity': 2.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dd': {'cloudCoverage': 0.88, 'cloudSpeed': 1.10, 'cloudSharpness': 0.20,
|
||||
'turbidity': 5.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dg': {'cloudCoverage': 0.28, 'cloudSpeed': 0.55, 'cloudSharpness': 0.70,
|
||||
'turbidity': 1.8, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'mm': {'cloudCoverage': 0.55, 'cloudSpeed': 0.85, 'cloudSharpness': 0.45,
|
||||
'turbidity': 3.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'br': {'cloudCoverage': 0.78, 'cloudSpeed': 1.40, 'cloudSharpness': 0.15,
|
||||
'turbidity': 6.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dl': {'cloudCoverage': 0.22, 'cloudSpeed': 0.25, 'cloudSharpness': 0.50,
|
||||
'turbidity': 1.5, 'starBrightness': 0.92, 'moonEnabled': True,
|
||||
'moonDir': (225, -55, 0)},
|
||||
'gs': {'cloudCoverage': 0.32, 'cloudSpeed': 0.90, 'cloudSharpness': 0.50,
|
||||
'turbidity': 3.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'estate': {'cloudCoverage': 0.38, 'cloudSpeed': 0.60, 'cloudSharpness': 0.55,
|
||||
'turbidity': 2.2, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'playground': {'cloudCoverage': 0.35, 'cloudSpeed': 0.65, 'cloudSharpness': 0.50,
|
||||
'turbidity': 2.4, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
# HQ zones: minimal sky (player rarely sees it indoors/dense area)
|
||||
'sellbot_hq': {'cloudCoverage': 0.95, 'cloudSpeed': 0.30, 'cloudSharpness': 0.05,
|
||||
'turbidity': 8.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'cashbot_hq': {'cloudCoverage': 0.85, 'cloudSpeed': 0.40, 'cloudSharpness': 0.10,
|
||||
'turbidity': 7.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'lawbot_hq': {'cloudCoverage': 0.92, 'cloudSpeed': 0.20, 'cloudSharpness': 0.10,
|
||||
'turbidity': 7.5, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'bossbot_hq': {'cloudCoverage': 0.99, 'cloudSpeed': 0.10, 'cloudSharpness': 0.05,
|
||||
'turbidity': 8.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'cog': {'cloudCoverage': 0.75, 'cloudSpeed': 0.50, 'cloudSharpness': 0.20,
|
||||
'turbidity': 6.0, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
# Street variants
|
||||
'tt_street': {'cloudCoverage': 0.40, 'cloudSpeed': 0.68, 'cloudSharpness': 0.55,
|
||||
'turbidity': 2.4, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dd_street': {'cloudCoverage': 0.90, 'cloudSpeed': 1.20, 'cloudSharpness': 0.18,
|
||||
'turbidity': 5.8, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dg_street': {'cloudCoverage': 0.24, 'cloudSpeed': 0.52, 'cloudSharpness': 0.72,
|
||||
'turbidity': 1.8, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'mm_street': {'cloudCoverage': 0.52, 'cloudSpeed': 0.88, 'cloudSharpness': 0.42,
|
||||
'turbidity': 3.6, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'br_street': {'cloudCoverage': 0.80, 'cloudSpeed': 1.50, 'cloudSharpness': 0.12,
|
||||
'turbidity': 6.2, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
'dl_street': {'cloudCoverage': 0.20, 'cloudSpeed': 0.22, 'cloudSharpness': 0.48,
|
||||
'turbidity': 1.5, 'starBrightness': 0.88, 'moonEnabled': True,
|
||||
'moonDir': (225, -55, 0)},
|
||||
'golf_course': {'cloudCoverage': 0.30, 'cloudSpeed': 0.65, 'cloudSharpness': 0.60,
|
||||
'turbidity': 2.2, 'starBrightness': 0.0, 'moonEnabled': False},
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ProceduralSky class
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class ProceduralSky:
|
||||
"""Manages a GLSL-driven procedural sky sphere.
|
||||
|
||||
One instance per hood / zone. OutdoorLighting creates and destroys it
|
||||
alongside the light rig.
|
||||
"""
|
||||
|
||||
_TASK_NAME = 'proceduralSkyTask'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._skyNp: NodePath | None = None
|
||||
self._time: float = 0.0
|
||||
self._activeStyle: str = 'playground'
|
||||
self._attached: bool = False
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def attach(self, parent: NodePath, style: str = 'playground') -> None:
|
||||
"""Create the sky sphere and attach it to *parent* (usually camera)."""
|
||||
if self._attached:
|
||||
return
|
||||
shader = _loadSkyShader()
|
||||
if shader is None:
|
||||
return # graceful fallback – sky model will be used instead
|
||||
|
||||
try:
|
||||
self._skyNp = _makeSkySphereMesh()
|
||||
self._skyNp.reparentTo(parent)
|
||||
self._skyNp.setDepthTest(False)
|
||||
self._skyNp.setDepthWrite(False)
|
||||
self._skyNp.setLightOff(1)
|
||||
# Ensure the procedural sky is drawn after any legacy/model sky that
|
||||
# might also live in the background bin.
|
||||
self._skyNp.setBin('background', 1000)
|
||||
self._skyNp.setTwoSided(True)
|
||||
self._skyNp.setShader(shader)
|
||||
self._skyNp.setShaderAuto(False)
|
||||
|
||||
self._activeStyle = style
|
||||
self._attached = True
|
||||
self._time = 0.0
|
||||
|
||||
# No update task here – OutdoorLighting's main task calls update().
|
||||
except Exception as e:
|
||||
import traceback; traceback.print_exc()
|
||||
self._skyNp = None
|
||||
|
||||
def update(self, spec: dict, timeOfDay: float = 12.0) -> None:
|
||||
"""Push zone spec parameters as shader uniforms."""
|
||||
if not self._attached or self._skyNp is None or self._skyNp.isEmpty():
|
||||
return
|
||||
|
||||
self._time += globalClock.getDt()
|
||||
|
||||
# Resolve sky sub-parameters (check spec first, fall back to defaults).
|
||||
style = self._activeStyle
|
||||
skyDef = _ZONE_SKY_DEFAULTS.get(style, _ZONE_SKY_DEFAULTS['playground'])
|
||||
cov = float(spec.get('cloudCoverage', skyDef.get('cloudCoverage', 0.4)))
|
||||
spd = float(spec.get('cloudSpeed', skyDef.get('cloudSpeed', 0.6)))
|
||||
sharp = float(spec.get('cloudSharpness', skyDef.get('cloudSharpness', 0.5)))
|
||||
turb = float(spec.get('turbidity', skyDef.get('turbidity', 2.5)))
|
||||
stars = float(spec.get('starBrightness', skyDef.get('starBrightness', 0.0)))
|
||||
moonOn = float(1 if spec.get('moonEnabled', skyDef.get('moonEnabled', False)) else 0)
|
||||
moonHpr = spec.get('moonDir', skyDef.get('moonDir', (0, -45, 0)))
|
||||
|
||||
# Sun direction: forward vector of key light (direction light shines).
|
||||
keyHpr = spec.get('keyHpr', (135, -42, 0))
|
||||
lightFwd = _hprToDir(keyHpr[0], keyHpr[1])
|
||||
# Sun position in sky = opposite of light direction (world space).
|
||||
sunDirWorld = Vec3(-lightFwd.x, -lightFwd.y, -lightFwd.z)
|
||||
|
||||
moonDirWorld = Vec3(0, 0, 1)
|
||||
if moonOn > 0.5:
|
||||
mfwd = _hprToDir(moonHpr[0], moonHpr[1])
|
||||
moonDirWorld = Vec3(-mfwd.x, -mfwd.y, -mfwd.z)
|
||||
|
||||
# Must match camera-space ``vDir`` in the shader (dome is under base.cam).
|
||||
sunDirCam = _dir_world_to_cam(sunDirWorld)
|
||||
moonDirCam = _dir_world_to_cam(moonDirWorld)
|
||||
|
||||
keyColor = Vec4(*spec.get('key', (1, 1, 1, 1)))
|
||||
clearColor = spec.get('clearColor', (0.4, 0.6, 0.85, 1.0))
|
||||
fogColorV = spec.get('fogColor', (0.6, 0.7, 0.85, 1.0))
|
||||
skyScaleV = spec.get('skyScale', (1, 1, 1, 1))
|
||||
|
||||
# Derive zenith and horizon colours from clearColor and fogColor.
|
||||
# zenith = clearColor (the "perfect overhead blue")
|
||||
# horizon = blend clearColor → fogColor
|
||||
zenith = Vec3(clearColor[0], clearColor[1], clearColor[2])
|
||||
horizon = Vec3(fogColorV[0] * 0.85, fogColorV[1] * 0.85, fogColorV[2] * 0.85)
|
||||
|
||||
# Day/night: fade stars from *world* sun elevation (stable when camera tilts).
|
||||
isDaytime = max(0.0, min(1.0, (sunDirWorld.z + 0.2) * 4.0))
|
||||
effectiveStars = stars * (1.0 - isDaytime)
|
||||
|
||||
# Sun blind strength: default 0.85 (strong cinematic glare), clamped 0–1
|
||||
blindStr = float(spec.get('sunBlindStrength', 0.85))
|
||||
blindStr = max(0.0, min(1.0, blindStr))
|
||||
|
||||
try:
|
||||
np = self._skyNp
|
||||
np.setShaderInput('sunDir', sunDirCam)
|
||||
np.setShaderInput('sunWorldElev', float(sunDirWorld.z))
|
||||
np.setShaderInput('sunColor', keyColor)
|
||||
np.setShaderInput('zenithColor', zenith)
|
||||
np.setShaderInput('horizonColor', horizon)
|
||||
np.setShaderInput('fogColor', Vec3(fogColorV[0], fogColorV[1], fogColorV[2]))
|
||||
np.setShaderInput('cloudCoverage', cov)
|
||||
np.setShaderInput('cloudSpeed', spd * _CLOUD_BASE_SPEED)
|
||||
np.setShaderInput('cloudSharpness', sharp)
|
||||
np.setShaderInput('turbidity', turb)
|
||||
np.setShaderInput('starBrightness', effectiveStars)
|
||||
np.setShaderInput('moonEnabled', moonOn)
|
||||
np.setShaderInput('moonDir', moonDirCam)
|
||||
np.setShaderInput('moonColor', Vec4(*spec.get('key', (0.5, 0.6, 1, 1))))
|
||||
np.setShaderInput('time', self._time)
|
||||
np.setShaderInput('skyScale', Vec4(*skyScaleV))
|
||||
# New feature uniforms
|
||||
np.setShaderInput('sunDiscEnabled', 1.0)
|
||||
np.setShaderInput('sunBlindStrength', blindStr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def setStyle(self, style: str) -> None:
|
||||
"""Update which zone sky defaults to use."""
|
||||
self._activeStyle = style
|
||||
|
||||
def isActive(self) -> bool:
|
||||
return self._attached and self._skyNp is not None and not self._skyNp.isEmpty()
|
||||
|
||||
def detach(self) -> None:
|
||||
"""Remove the sky sphere and clean up."""
|
||||
if self._skyNp and not self._skyNp.isEmpty():
|
||||
self._skyNp.removeNode()
|
||||
self._skyNp = None
|
||||
self._attached = False
|
||||
self._time = 0.0
|
||||
|
|
@ -51,8 +51,13 @@ class TTHood(ToonHood.ToonHood):
|
|||
def startSpookySky(self):
|
||||
if hasattr(self, 'sky') and self.sky:
|
||||
self.stopSky()
|
||||
self.sky = loader.loadModel(self.spookySkyFile)
|
||||
self.sky.setTag('sky', 'Halloween')
|
||||
# Parent Hood._loadSkyModel handles deleted phase props / ProceduralSky placeholder.
|
||||
self.sky = self._loadSkyModel(self.spookySkyFile, halloween=True)
|
||||
try:
|
||||
if self.sky.getName() in ('legacySkyDisabled', 'missingSkyPlaceholder'):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
self.sky.setScale(1.0)
|
||||
self.sky.setDepthTest(0)
|
||||
self.sky.setDepthWrite(0)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from toontown.safezone import TTTreasurePlannerAI
|
|||
from toontown.classicchars import DistributedMickeyAI
|
||||
from toontown.safezone import ButterflyGlobals
|
||||
from direct.task import Task
|
||||
from toontown.safezone.DistributedTTCCraneSandboxAI import DistributedTTCCraneSandboxAI
|
||||
|
||||
class TTHoodDataAI(HoodDataAI.HoodDataAI):
|
||||
notify = DirectNotifyGlobal.directNotify.newCategory('TTHoodDataAI')
|
||||
|
|
@ -30,6 +31,10 @@ class TTHoodDataAI(HoodDataAI.HoodDataAI):
|
|||
self.classicChar.generateWithRequired(self.zoneId)
|
||||
self.classicChar.start()
|
||||
self.addDistObj(self.classicChar)
|
||||
self.ttcCraneSandbox = DistributedTTCCraneSandboxAI(self.air)
|
||||
self.ttcCraneSandbox.generateWithRequired(self.zoneId)
|
||||
self.addDistObj(self.ttcCraneSandbox)
|
||||
self.ttcCraneSandbox.spawnCranesAndSafes()
|
||||
self.createButterflies(ButterflyGlobals.TTC)
|
||||
if simbase.blinkTrolley:
|
||||
taskMgr.doMethodLater(0.5, self._deleteTrolley, 'deleteTrolley')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
// Vertex shader for the HDR scene composite quad rendered through a
|
||||
// 3D orthographic FilterManager camera. Unlike the render2dp overlay
|
||||
// path (sunrays.vert.glsl), the quad lives in model-space (XZ plane,
|
||||
// Y=0), so the full ModelViewProjection transform is required to map it
|
||||
// to clip space correctly.
|
||||
#version 130
|
||||
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
in vec4 p3d_Vertex;
|
||||
in vec2 p3d_MultiTexCoord0;
|
||||
out vec2 uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
|
||||
uv = p3d_MultiTexCoord0;
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
// Scene post-processing composite fragment shader.
|
||||
//
|
||||
// Single-pass pipeline that reads a FilterManager-captured scene and applies:
|
||||
// 1. Depth-buffer-occluded screen-space god rays (Kenny Mitchell technique,
|
||||
// GPU Gems 3 Ch. 13). Sky pixels (depth ≈ 1.0) let light through;
|
||||
// solid geometry pixels block it, creating real geometry-cast light shafts.
|
||||
// 2. Approximate single-pass bloom (bright-pass + large kernel box blur
|
||||
// at multiple offsets — not physically perfect but fast and convincing).
|
||||
// 3. ACES filmic tonemapping with per-zone exposure adjustment.
|
||||
//
|
||||
// Uniforms set by OutdoorLighting._setupPostProcess():
|
||||
// sceneColor – RGBA scene texture (float16 or RGBA8 offscreen; exposure+ACES below)
|
||||
// sceneDepth – depth texture matching sceneColor dimensions
|
||||
// sunScreenPos – sun position in [0,1] screen UV space
|
||||
// rayColor – god-ray tint (RGBA)
|
||||
// rayIntensity – master ray strength (0 = off)
|
||||
// bloomIntensity – bloom strength (0 = off)
|
||||
// bloomThreshold – luminance threshold for bright-pass
|
||||
// exposure – scene exposure scalar (default 1.0)
|
||||
// tonemapEnabled – 0 = bypass tonemap (linear output), 1 = ACES
|
||||
// vignetteStrength – 0 = off, 0.25 = subtle, 1 = strong
|
||||
// time – animation seconds (unused here; reserved for shimmer)
|
||||
// texelSize – vec2(1/width, 1/height) for blur offsets
|
||||
#version 130
|
||||
|
||||
uniform sampler2D sceneColor;
|
||||
uniform sampler2D sceneDepth;
|
||||
uniform vec2 sunScreenPos;
|
||||
uniform vec4 rayColor;
|
||||
uniform float rayIntensity;
|
||||
// Bloom is temporarily disabled in code to avoid a Panda3D shader input
|
||||
// assertion on some drivers; keep the effect path available for later.
|
||||
uniform float exposure;
|
||||
uniform float tonemapEnabled;
|
||||
uniform float vignetteStrength;
|
||||
uniform float time;
|
||||
uniform vec2 texelSize;
|
||||
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
|
||||
// ── ACES filmic tonemapper ───────────────────────────────────────────────────
|
||||
// Fitted curve by Krzysztof Narkowicz (2015). Very close to the full ACES
|
||||
// reference at a fraction of the cost.
|
||||
vec3 acesTonemap(vec3 x) {
|
||||
const float a = 2.51;
|
||||
const float b = 0.03;
|
||||
const float c = 2.43;
|
||||
const float d = 0.59;
|
||||
const float e = 0.14;
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
// ── Reinhard (per-channel) ───────────────────────────────────────────────────
|
||||
vec3 reinhardTonemap(vec3 x) {
|
||||
return x / (1.0 + x);
|
||||
}
|
||||
|
||||
// ── Luminance helper ─────────────────────────────────────────────────────────
|
||||
float luminance(vec3 c) {
|
||||
return dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
// ── Screen-space depth-occluded god rays ─────────────────────────────────────
|
||||
//
|
||||
// Algorithm: march from the current pixel toward the sun position in screen
|
||||
// space (NUM_SAMPLES steps). At each step sample the depth buffer.
|
||||
// depth == 1.0 (or very close) → sky pixel → sun is visible → accumulate
|
||||
// depth < DEPTH_THRESHOLD → geometry pixel → occluded → skip
|
||||
//
|
||||
// The accumulated value is weighted by an exponential decay so samples nearer
|
||||
// the sun contribute more. The classic GPU Gems 3 weighting applies.
|
||||
vec3 godRays(vec2 pixelUV, float intensity) {
|
||||
if (intensity <= 0.001) return vec3(0.0);
|
||||
|
||||
const int NUM_SAMPLES = 96;
|
||||
const float DECAY = 0.966;
|
||||
const float DENSITY = 0.84;
|
||||
const float WEIGHT = 0.45;
|
||||
const float EXPOSURE_RAY = 0.16;
|
||||
const float DEPTH_THRESHOLD = 0.9998; // sky depth threshold
|
||||
|
||||
// Cull when the sun is completely off-screen to avoid aliasing artefacts.
|
||||
vec2 sunEdgeDist = min(sunScreenPos, 1.0 - sunScreenPos);
|
||||
float edgeFade = smoothstep(0.0, 0.08, min(sunEdgeDist.x, sunEdgeDist.y));
|
||||
if (edgeFade <= 0.0) return vec3(0.0);
|
||||
|
||||
vec2 delta = (pixelUV - sunScreenPos) * (DENSITY / float(NUM_SAMPLES));
|
||||
vec2 sampleUV = pixelUV;
|
||||
float illum = 0.0;
|
||||
float decay = 1.0;
|
||||
|
||||
for (int i = 0; i < NUM_SAMPLES; ++i) {
|
||||
sampleUV -= delta;
|
||||
// Clamp so we don't sample outside the texture.
|
||||
vec2 cUV = clamp(sampleUV, vec2(0.001), vec2(0.999));
|
||||
float d = texture(sceneDepth, cUV).r;
|
||||
// Sky pixels (d ≥ DEPTH_THRESHOLD) are unoccluded → contribute.
|
||||
float sky = step(DEPTH_THRESHOLD, d);
|
||||
illum += sky * decay * WEIGHT;
|
||||
decay *= DECAY;
|
||||
}
|
||||
illum *= EXPOSURE_RAY * intensity * edgeFade;
|
||||
|
||||
return rayColor.rgb * illum;
|
||||
}
|
||||
|
||||
// ── Single-pass approximate bloom ───────────────────────────────────────────
|
||||
//
|
||||
// Extracts bright pixels then blurs with a two-ring sample pattern (Poisson
|
||||
// disc approximation). Not as smooth as multi-pass Gaussian but avoids the
|
||||
// need for ping-pong buffers, keeping us in one FilterManager pass.
|
||||
vec3 bloom(vec2 pixUV, float threshold, float intensity) {
|
||||
if (intensity <= 0.001) return vec3(0.0);
|
||||
|
||||
vec3 acc = vec3(0.0);
|
||||
float total = 0.0;
|
||||
|
||||
// Two rings: inner (4 samples) + outer (8 samples)
|
||||
// Offsets are in texel units; scale drives blur radius.
|
||||
float blurRadius = mix(3.0, 9.0, intensity);
|
||||
|
||||
vec2 offsets[12];
|
||||
// Inner ring
|
||||
offsets[0] = vec2( 1.0, 0.0);
|
||||
offsets[1] = vec2(-1.0, 0.0);
|
||||
offsets[2] = vec2( 0.0, 1.0);
|
||||
offsets[3] = vec2( 0.0, -1.0);
|
||||
// Mid ring
|
||||
offsets[4] = vec2( 1.5, 1.5);
|
||||
offsets[5] = vec2(-1.5, 1.5);
|
||||
offsets[6] = vec2( 1.5, -1.5);
|
||||
offsets[7] = vec2(-1.5, -1.5);
|
||||
// Outer ring
|
||||
offsets[8] = vec2( 3.0, 0.0);
|
||||
offsets[9] = vec2(-3.0, 0.0);
|
||||
offsets[10] = vec2( 0.0, 3.0);
|
||||
offsets[11] = vec2( 0.0, -3.0);
|
||||
|
||||
float weights[12];
|
||||
weights[0] = 1.00; weights[1] = 1.00;
|
||||
weights[2] = 1.00; weights[3] = 1.00;
|
||||
weights[4] = 0.70; weights[5] = 0.70;
|
||||
weights[6] = 0.70; weights[7] = 0.70;
|
||||
weights[8] = 0.35; weights[9] = 0.35;
|
||||
weights[10] = 0.35; weights[11] = 0.35;
|
||||
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
vec2 sUV = pixUV + offsets[i] * texelSize * blurRadius;
|
||||
vec3 col = texture(sceneColor, sUV).rgb;
|
||||
float lum = luminance(col);
|
||||
float bright = max(0.0, lum - threshold);
|
||||
acc += col * bright * weights[i];
|
||||
total += weights[i];
|
||||
}
|
||||
if (total > 0.0) acc /= total;
|
||||
|
||||
return acc * intensity * 1.4;
|
||||
}
|
||||
|
||||
// ── Vignette ────────────────────────────────────────────────────────────────
|
||||
float vignette(vec2 u, float strength) {
|
||||
vec2 d = u - 0.5;
|
||||
return 1.0 - dot(d, d) * strength * 3.2;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
vec4 sceneRGBA = texture(sceneColor, uv);
|
||||
vec3 col = sceneRGBA.rgb * exposure;
|
||||
|
||||
// ── God rays ─────────────────────────────────────────────────────────────
|
||||
col += godRays(uv, rayIntensity);
|
||||
|
||||
// ── Bloom ────────────────────────────────────────────────────────────────
|
||||
// Disabled for now (see note above).
|
||||
col += bloom(uv, 1.0, 0.0);
|
||||
|
||||
// ── Tonemapping ──────────────────────────────────────────────────────────
|
||||
if (tonemapEnabled > 0.5) {
|
||||
col = acesTonemap(col);
|
||||
} else {
|
||||
col = clamp(col, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// ── Vignette ─────────────────────────────────────────────────────────────
|
||||
if (vignetteStrength > 0.001) {
|
||||
col *= clamp(vignette(uv, vignetteStrength), 0.0, 1.0);
|
||||
}
|
||||
|
||||
fragColor = vec4(col, sceneRGBA.a);
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
// Procedural physically-based sky fragment shader – MASSIVE REWRITE
|
||||
//
|
||||
// Implements (new and improved):
|
||||
// • Rayleigh + Mie scattering – accurate wavelength-dependent blue-sky gradient
|
||||
// • Wide sunset/sunrise horizon corona (orange-pink band near horizon)
|
||||
// • Ozone absorption – cyan/yellow sky transition near sunset
|
||||
// • Horizon haze / atmospheric extinction with turbidity control
|
||||
// • VISIBLE SUN DISC with:
|
||||
// – Physical angular radius (~0.27°)
|
||||
// – Limb darkening (edges slightly darker than centre)
|
||||
// – Colour shifts noon→white-yellow, sunset→deep orange-red
|
||||
// – Multi-layer exponential corona / aureole
|
||||
// – Subtle vertical lens-flare streak
|
||||
// – BLINDING GLARE when staring directly into the sun (chromatic aberration glow)
|
||||
// • Volumetric-looking FBM cumulus clouds with domain warping:
|
||||
// – Two-pass domain warp (organic, non-repetitive shapes)
|
||||
// – Multi-layer depth sampling (distinct top/middle/base layers)
|
||||
// – Sun back-lighting (silver lining on edges facing sun)
|
||||
// – Sunset orange-pink underbelly glow
|
||||
// – Moon-lit night-time clouds (cool blue-grey)
|
||||
// – Per-zone coverage, speed, sharpness
|
||||
// • Twinkling star field:
|
||||
// – Per-star unique twinkle frequency & phase
|
||||
// – Four spectral classes: blue-white / white / yellow-white / warm
|
||||
// – Occasional "sparkle" diffraction cross on bright stars
|
||||
// – Fades behind clouds and near the sun
|
||||
// • MOON disc with:
|
||||
// – Surface noise detail (craters)
|
||||
// – Limb darkening
|
||||
// – Multi-layer corona glow
|
||||
// – Cloud occlusion
|
||||
// • Zone colour scale (skyScale) applied to final output
|
||||
// • Reinhard tonemapping + gamma lift for natural HDR-to-LDR
|
||||
//
|
||||
// Uniforms set by ProceduralSky.update():
|
||||
// sunDir – world-space direction TOWARD the sun (normalised)
|
||||
// sunColor – key light colour; used for Mie glow tint
|
||||
// zenithColor – deep sky colour at zenith (zone)
|
||||
// horizonColor – sky colour at horizon (zone)
|
||||
// fogColor – atmospheric haze/fog tint (zone)
|
||||
// cloudCoverage – 0.0 (clear) … 1.0 (overcast) (zone)
|
||||
// cloudSpeed – cloud animation multiplier (zone)
|
||||
// cloudSharpness – edge sharpness 0.0 (fluffy) … 1.0 (sharp) (zone)
|
||||
// turbidity – Mie strength 1.0 (crisp) … 8.0 (hazy) (zone)
|
||||
// starBrightness – star intensity; 0 in day, up to 1 at night (zone)
|
||||
// moonEnabled – 1.0 = draw a moon disc
|
||||
// moonDir – world-space direction toward moon
|
||||
// moonColor – moon disc tint
|
||||
// time – animation seconds (drives cloud drift + star twinkle)
|
||||
// skyScale – vec4 colour multiplier (zone tint)
|
||||
// sunDiscEnabled – 1.0 = draw visible sun disc (default 1)
|
||||
// sunBlindStrength – 0.0–1.0 blinding glare when staring into sun
|
||||
// sunWorldElev – sun direction Z in *world* space [-1..1] (time-of-day);
|
||||
// separate from sunDir so atmosphere does not swim when the camera tilts
|
||||
#version 130
|
||||
|
||||
// ── Atmosphere uniforms ────────────────────────────────────────────────────
|
||||
uniform vec3 sunDir;
|
||||
uniform float sunWorldElev;
|
||||
uniform vec4 sunColor;
|
||||
uniform vec3 zenithColor;
|
||||
uniform vec3 horizonColor;
|
||||
uniform vec3 fogColor;
|
||||
// ── Cloud uniforms ─────────────────────────────────────────────────────────
|
||||
uniform float cloudCoverage;
|
||||
uniform float cloudSpeed;
|
||||
uniform float cloudSharpness;
|
||||
// ── Scattering ────────────────────────────────────────────────────────────
|
||||
uniform float turbidity;
|
||||
// ── Night sky ─────────────────────────────────────────────────────────────
|
||||
uniform float starBrightness;
|
||||
uniform float moonEnabled;
|
||||
uniform vec3 moonDir;
|
||||
uniform vec4 moonColor;
|
||||
// ── Animation ─────────────────────────────────────────────────────────────
|
||||
uniform float time;
|
||||
uniform vec4 skyScale;
|
||||
// ── New features ──────────────────────────────────────────────────────────
|
||||
uniform float sunDiscEnabled; // 1.0 = render sun disc + corona
|
||||
uniform float sunBlindStrength; // 0–1 glare when staring at sun
|
||||
|
||||
in vec3 vDir;
|
||||
in vec2 vUV;
|
||||
out vec4 fragColor;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Noise / hash utilities
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
float _hash(vec2 p) {
|
||||
p = fract(p * vec2(127.1, 311.7));
|
||||
p += dot(p, p + 19.19);
|
||||
return fract(p.x * p.y);
|
||||
}
|
||||
|
||||
float _vnoise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(
|
||||
mix(_hash(i), _hash(i + vec2(1.0, 0.0)), u.x),
|
||||
mix(_hash(i + vec2(0.0, 1.0)), _hash(i + vec2(1.0, 1.0)), u.x),
|
||||
u.y
|
||||
);
|
||||
}
|
||||
|
||||
// 8-octave FBM with per-octave rotation (breaks axis-aligned repetition)
|
||||
float _fbm(vec2 p) {
|
||||
float v = 0.0;
|
||||
float amp = 0.5;
|
||||
mat2 rot = mat2(1.6, 1.2, -1.2, 1.6);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
v += amp * _vnoise(p);
|
||||
p = rot * p * 2.1;
|
||||
amp *= 0.46;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Phase functions
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
float _mie(float cosA, float g) {
|
||||
float g2 = g * g;
|
||||
return (1.0 - g2) / pow(max(1.0e-4, 1.0 + g2 - 2.0 * g * cosA), 1.5) * 0.25;
|
||||
}
|
||||
|
||||
float _rayleigh(float cosA) {
|
||||
return 0.75 * (1.0 + cosA * cosA);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Main
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
void main() {
|
||||
vec3 dir = normalize(vDir);
|
||||
float elev = dir.z;
|
||||
float elevAbs = abs(elev);
|
||||
float cosTheta = dot(dir, sunDir);
|
||||
// World-space sun height (stable when camera pitches/yaws); sunDir is camera-space for dots.
|
||||
float sunElevW = sunWorldElev;
|
||||
// sunPower: 0 when sun is on or below horizon, rises to 1 near zenith
|
||||
float sunPower = clamp(sunElevW * 3.5 + 0.28, 0.0, 1.0);
|
||||
|
||||
// ── Rayleigh scattering ──────────────────────────────────────────────
|
||||
// Wavelength-dependent (shorter λ = blue scatters more)
|
||||
vec3 rayleigh_wl = vec3(0.26, 0.50, 1.00);
|
||||
float rayleighPhase = _rayleigh(cosTheta);
|
||||
vec3 rayleighCol = rayleigh_wl * rayleighPhase * mix(0.28, 1.05, sunPower);
|
||||
|
||||
// ── Mie scattering (turbidity-driven forward halo) ───────────────────
|
||||
float g = max(0.58, 0.84 - turbidity * 0.020);
|
||||
float mieStr = turbidity * 0.13;
|
||||
float mieGlow = _mie(cosTheta, g) * mieStr;
|
||||
vec3 mieCol = sunColor.rgb * mieGlow;
|
||||
|
||||
// ── Wide horizon corona at sunset/sunrise ────────────────────────────
|
||||
// Horizontal dot: measures how closely dir aligns with sun's azimuth at horizon
|
||||
vec2 sunAz = vec2(sunDir.x, sunDir.y);
|
||||
float sunAzLen = max(0.001, length(sunAz));
|
||||
float horizDot = dot(vec2(dir.x, dir.y), sunAz / sunAzLen);
|
||||
float horizBand = pow(max(0.0, horizDot), 3.5)
|
||||
* max(0.0, 1.0 - abs(sunElevW) * 3.8) // only near horizon
|
||||
* (1.0 - abs(elev) * 3.0) // fade away from horizon line
|
||||
* 0.70;
|
||||
vec3 horizGlowCol = mix(
|
||||
vec3(1.00, 0.42, 0.06), // deep orange at low elevation
|
||||
vec3(1.00, 0.80, 0.42), // golden higher up
|
||||
clamp(sunPower * 1.5, 0.0, 1.0)
|
||||
) * horizBand;
|
||||
|
||||
// ── Ozone absorption ─────────────────────────────────────────────────
|
||||
float ozone = max(0.0, 1.0 - elevAbs * 1.55);
|
||||
vec3 ozoneCol = vec3(0.00, 0.13, 0.22) * ozone * sunPower;
|
||||
|
||||
// ── Sky gradient (zenith → horizon) ──────────────────────────────────
|
||||
float horizonT = pow(clamp(elev * 1.25 + 0.14, 0.0, 1.0), 0.50);
|
||||
vec3 gradCol = mix(horizonColor, zenithColor, horizonT);
|
||||
|
||||
// Sunset/sunrise band: orange-pink near horizon, purple higher up
|
||||
float sunsetBand = max(0.0, 1.0 - abs(sunElevW) * 2.2) * (1.0 - horizonT * 0.75);
|
||||
vec3 sunsetTint = mix(
|
||||
vec3(1.00, 0.38, 0.05), // near-horizon orange
|
||||
vec3(0.55, 0.25, 0.72), // purple higher
|
||||
horizonT
|
||||
) * sunsetBand * 0.62;
|
||||
gradCol += sunsetTint;
|
||||
|
||||
// Assemble base sky
|
||||
vec3 sky = gradCol
|
||||
+ rayleighCol * 0.42
|
||||
+ mieCol
|
||||
+ ozoneCol
|
||||
+ horizGlowCol;
|
||||
|
||||
// ── Horizon haze (atmospheric extinction) ────────────────────────────
|
||||
float hazeT = exp(-max(0.0, elev) * 5.2 * turbidity * 0.27);
|
||||
sky = mix(sky, fogColor, hazeT * 0.52);
|
||||
|
||||
// ── Below horizon: fade to dark fog ──────────────────────────────────
|
||||
if (elev < 0.0) {
|
||||
float below = clamp(-elev * 9.5, 0.0, 1.0);
|
||||
sky = mix(sky, fogColor * 0.48, below);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Volumetric clouds
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
float cloudAlpha = 0.0;
|
||||
vec3 cloudRGB = vec3(1.0);
|
||||
|
||||
if (cloudCoverage > 0.02 && elev > -0.07) {
|
||||
// Perspective projection onto cloud layer at ~1 km altitude
|
||||
float layerScale = 1.0 / max(0.05, elev + 0.05);
|
||||
vec2 cloudUV = vec2(dir.x, dir.y) * layerScale * 0.36
|
||||
+ vec2(time * cloudSpeed * 0.00115, time * cloudSpeed * 0.00045);
|
||||
|
||||
// ── Domain warping pass 1 (large-scale organic distortion) ───────
|
||||
vec2 warp1 = vec2(
|
||||
_fbm(cloudUV * 1.55),
|
||||
_fbm(cloudUV * 1.55 + vec2(5.20, 1.30))
|
||||
) * 0.32;
|
||||
|
||||
// ── Domain warping pass 2 (medium-scale detail) ──────────────────
|
||||
vec2 warp2 = vec2(
|
||||
_fbm(cloudUV * 0.82 + vec2(1.70, 9.20)),
|
||||
_fbm(cloudUV * 0.82 + vec2(8.30, 2.80))
|
||||
) * 0.14;
|
||||
|
||||
vec2 warpedUV = cloudUV + warp1 + warp2;
|
||||
|
||||
// Primary cloud density field
|
||||
float density = _fbm(warpedUV * 2.35);
|
||||
|
||||
// Coverage → threshold
|
||||
float threshold = 1.0 - cloudCoverage * 0.76;
|
||||
float raw = density - threshold;
|
||||
float edgeWidth = mix(0.32, 0.045, cloudSharpness);
|
||||
float shaped = smoothstep(0.0, edgeWidth, raw);
|
||||
|
||||
// Horizon fade (perspective stretch makes low-angle clouds blur badly)
|
||||
shaped *= smoothstep(-0.04, 0.18, elev);
|
||||
|
||||
// ── Multi-layer depth sampling for 3-D cloud body feel ──────────
|
||||
float densityMid = _fbm(warpedUV * 4.00 + vec2(1.70, 3.10));
|
||||
float densityFine = _fbm(warpedUV * 8.50 + vec2(-2.30, 0.80));
|
||||
// volDepth: 0 = outer edge, 1 = deep interior
|
||||
float volDepth = clamp(densityMid * 0.45 + densityFine * 0.18, 0.0, 1.0);
|
||||
|
||||
// ── Cloud illumination ───────────────────────────────────────────
|
||||
float sunDot = max(0.0, dot(dir, sunDir));
|
||||
float shadowing = clamp(1.0 - shaped * 0.68, 0.16, 1.0);
|
||||
|
||||
// Top surface: brightly lit by direct sun
|
||||
vec3 litTop = mix(vec3(0.96, 0.97, 1.00), sunColor.rgb * 1.20, 0.20);
|
||||
|
||||
// Cloud base: deeper interior → darker grey-blue shadow
|
||||
float depthSh = mix(0.36, 0.62, volDepth);
|
||||
vec3 litBase = litTop * vec3(depthSh * 0.88, depthSh * 0.93, depthSh * 1.04);
|
||||
|
||||
// Sunset underbelly: orange-pink glow when sun is near horizon
|
||||
float sunsetC = max(0.0, 1.0 - abs(sunElevW) * 4.0) * sunPower;
|
||||
vec3 sunsetBelly = mix(
|
||||
vec3(1.0, 0.55, 0.22),
|
||||
vec3(1.0, 0.78, 0.52),
|
||||
clamp(sunDot, 0.0, 1.0)
|
||||
) * sunsetC * 0.75;
|
||||
litBase += sunsetBelly;
|
||||
|
||||
// Second FBM sample for vertical shading variation
|
||||
float baseShade = _fbm(warpedUV * 2.35 + vec2(0.30, 0.15)) * 0.72 + 0.28;
|
||||
cloudRGB = mix(litBase, litTop, baseShade * shadowing);
|
||||
|
||||
// Silver lining: bright backlit halo on sun-facing cloud edges
|
||||
float silverEdge = smoothstep(edgeWidth * 0.55, 0.0, raw) * sunDot;
|
||||
cloudRGB += vec3(0.72, 0.64, 0.46) * silverEdge * sunPower * 0.70;
|
||||
|
||||
// Night / moon-lit clouds (cool dim blue-grey)
|
||||
if (moonEnabled > 0.5 && sunPower < 0.30) {
|
||||
float mnFade = clamp((0.30 - sunPower) * 4.0, 0.0, 1.0);
|
||||
float mnDot = max(0.0, dot(dir, normalize(moonDir)));
|
||||
vec3 mnLight = vec3(0.35, 0.42, 0.60) * mnDot * 0.38;
|
||||
float nDepth = mix(0.06, 0.52, shaped);
|
||||
cloudRGB = mix(cloudRGB, vec3(0.05, 0.07, 0.14) + mnLight * shaped,
|
||||
mnFade * nDepth);
|
||||
}
|
||||
|
||||
cloudAlpha = shaped;
|
||||
}
|
||||
|
||||
sky = mix(sky, cloudRGB, cloudAlpha);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Twinkling star field
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (starBrightness > 0.004 && elev > 0.02) {
|
||||
// Quantise direction → star cells (each has at most 1 star)
|
||||
vec3 snap = floor(dir * 210.0) / 210.0;
|
||||
float seed = _hash(snap.xy * vec2(43.0, 127.0) + snap.z * 59.0);
|
||||
|
||||
// Star brightness / size (varies per cell)
|
||||
float szSeed = _hash(snap.yx * vec2(71.0, 23.0) + snap.z * 17.0);
|
||||
float exponent = mix(430.0, 680.0, szSeed);
|
||||
float rawBrt = pow(seed, exponent) * mix(2.4, 5.2, szSeed);
|
||||
|
||||
// ── Twinkle: per-star unique frequency and phase ─────────────────
|
||||
float twSeed = _hash(snap.xy * 33.0 + snap.z * 71.0);
|
||||
float twFreq = mix(0.35, 4.0, twSeed);
|
||||
float twPhase = twSeed * 6.28318530;
|
||||
// Primary twinkle
|
||||
float twinkle = 0.62 + 0.38 * sin(time * twFreq + twPhase);
|
||||
// Secondary high-frequency shimmer on bright stars
|
||||
float shimmer = 1.0 + 0.15 * sin(time * twFreq * 3.3 + twPhase * 1.7);
|
||||
float star = rawBrt * twinkle * shimmer;
|
||||
|
||||
// ── Star spectral class ──────────────────────────────────────────
|
||||
float colSeed = _hash(snap.yz * vec2(37.0, 53.0));
|
||||
vec3 starCol;
|
||||
if (colSeed < 0.22) starCol = vec3(0.76, 0.84, 1.00); // O/B blue-white
|
||||
else if (colSeed < 0.48) starCol = vec3(1.00, 1.00, 0.94); // A/F white
|
||||
else if (colSeed < 0.74) starCol = vec3(1.00, 0.95, 0.70); // G yellow-white
|
||||
else starCol = vec3(1.00, 0.75, 0.55); // K/M warm orange
|
||||
|
||||
// Dim near sun's position in sky
|
||||
float nearSun = max(0.0, dot(dir, sunDir));
|
||||
star *= max(0.0, 1.0 - nearSun * nearSun * 4.0);
|
||||
|
||||
// Fade behind clouds
|
||||
star *= max(0.0, 1.0 - cloudAlpha * 1.2);
|
||||
|
||||
sky += starCol * star * starBrightness;
|
||||
|
||||
// ── Occasional bright "sparkle" with diffraction cross ───────────
|
||||
if (seed > 0.9982) {
|
||||
float sparkle = (seed - 0.9982) / 0.0018;
|
||||
sparkle = sparkle * sparkle * twinkle * twinkle;
|
||||
// Cross arms: exponential falloff from snap position
|
||||
float crossW = 0.0055;
|
||||
float arm_h = exp(-abs(dir.x - snap.x) / crossW);
|
||||
float arm_v = exp(-abs(dir.y - snap.y) / crossW);
|
||||
float cross2 = (arm_h + arm_v) * 0.5;
|
||||
sky += starCol * cross2 * sparkle * starBrightness * 0.50
|
||||
* max(0.0, 1.0 - cloudAlpha * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Sun disc + corona + blinding glare
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (sunDiscEnabled > 0.5 && sunElevW > -0.14) {
|
||||
float angDist = acos(clamp(cosTheta, -1.0, 1.0));
|
||||
float sunR = 0.0048; // angular radius of sun disc (~0.275°)
|
||||
|
||||
// ── Solar disc with limb darkening ───────────────────────────────
|
||||
float limbT = clamp(1.0 - angDist / sunR, 0.0, 1.0);
|
||||
float limb = smoothstep(0.0, 1.0, limbT) * step(angDist, sunR * 1.30);
|
||||
float limbDrk = mix(0.68, 1.0, limbT); // edges 32% darker than centre
|
||||
|
||||
// Colour: white-yellow at noon → deep orange-red at horizon
|
||||
float sunsetT = clamp(1.0 - sunElevW * 5.5, 0.0, 1.0);
|
||||
vec3 discCol = mix(
|
||||
vec3(1.00, 0.97, 0.82) * 5.0, // noon: brilliant white-yellow
|
||||
vec3(1.00, 0.44, 0.05) * 2.8, // sunset: deep orange-red
|
||||
sunsetT
|
||||
) * limbDrk;
|
||||
|
||||
// ── Multi-layer corona / aureole ─────────────────────────────────
|
||||
float c1 = exp(-angDist * 340.0) * 1.60;
|
||||
float c2 = exp(-angDist * 95.0) * 0.70;
|
||||
float c3 = exp(-angDist * 30.0) * 0.32;
|
||||
float c4 = exp(-angDist * 8.5) * 0.12;
|
||||
vec3 coronaCol = sunColor.rgb * (c1 + c2 + c3 + c4) * sunPower;
|
||||
|
||||
// ── Vertical diffraction streak ──────────────────────────────────
|
||||
float streak = exp(-abs(dir.z - sunDir.z) * 90.0)
|
||||
* exp(-max(0.0, 1.0 - cosTheta) * 150.0)
|
||||
* 0.28 * sunPower;
|
||||
vec3 streakCol = sunColor.rgb * streak;
|
||||
|
||||
// ── Blinding glare + chromatic aberration ────────────────────────
|
||||
float blindAmt = pow(max(0.0, cosTheta), 55.0) * sunBlindStrength * sunPower;
|
||||
// Slightly wider red channel for chromatic effect
|
||||
vec3 blindCol = vec3(
|
||||
pow(max(0.0, cosTheta), 38.0) * sunBlindStrength * sunPower * 1.35,
|
||||
blindAmt * 0.90,
|
||||
blindAmt * 0.65
|
||||
);
|
||||
|
||||
// ── Visibility: blocked by clouds, clipped below horizon ─────────
|
||||
float sunVis = (1.0 - cloudAlpha * 0.93)
|
||||
* clamp((sunElevW + 0.12) * 7.5, 0.0, 1.0);
|
||||
|
||||
sky += (discCol * limb + coronaCol + streakCol + blindCol) * sunVis;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Moon disc + surface detail + corona
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if (moonEnabled > 0.5) {
|
||||
vec3 mDir = normalize(moonDir);
|
||||
float moonDot = dot(dir, mDir);
|
||||
float moonR = 0.9990; // cos(~2.56°)
|
||||
float moonDisc = smoothstep(moonR, moonR + 0.0008, moonDot);
|
||||
|
||||
// Surface noise → subtle crater/mare variation
|
||||
vec3 mTang = normalize(dir - mDir * moonDot);
|
||||
float mDetail = _vnoise(vec2(mTang.x * 550.0 + 30.0,
|
||||
mTang.z * 550.0 + 70.0)) * 0.18 + 0.82;
|
||||
|
||||
// Limb darkening
|
||||
float mLimb = mix(0.65, 1.0,
|
||||
clamp(1.0 - max(0.0, 1.0 - moonDot) * 3.0, 0.0, 1.0));
|
||||
vec3 moonSurf = vec3(0.88, 0.88, 0.80) * mDetail * mLimb;
|
||||
|
||||
// Multi-layer glow
|
||||
float mH1 = exp(-max(0.0, 1.0 - moonDot) * 110.0) * 0.30;
|
||||
float mH2 = exp(-max(0.0, 1.0 - moonDot) * 24.0) * 0.09;
|
||||
float mH3 = exp(-max(0.0, 1.0 - moonDot) * 6.0) * 0.03;
|
||||
vec3 moonGlow = moonColor.rgb * (mH1 + mH2 + mH3) * 0.85;
|
||||
|
||||
float moonVis = max(0.0, 1.0 - cloudAlpha * 0.90);
|
||||
sky = mix(sky, moonSurf * 1.55, moonDisc * moonVis);
|
||||
sky += moonGlow * moonVis;
|
||||
}
|
||||
|
||||
// ── Zone colour scale ─────────────────────────────────────────────────
|
||||
sky *= skyScale.rgb;
|
||||
|
||||
// ── Tonemapping: Reinhard + mild gamma lift ───────────────────────────
|
||||
sky = sky / (sky + vec3(0.72)); // soft knee HDR clamp
|
||||
sky = pow(clamp(sky, 0.0, 1.0), vec3(1.0 / 1.15)); // slight gamma lift
|
||||
|
||||
fragColor = vec4(sky, 1.0);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Procedural sky sphere vertex shader.
|
||||
//
|
||||
// The sky is rendered on a large sphere (~950 unit radius) that follows the
|
||||
// camera. Because the sphere is centred at the camera in model space, every
|
||||
// vertex position is already a world-space ray direction. The fragment shader
|
||||
// uses that direction to compute atmospheric scattering colour and clouds.
|
||||
//
|
||||
// Panda3D coordinate convention: Y = forward, Z = up, X = right.
|
||||
#version 130
|
||||
|
||||
in vec4 p3d_Vertex;
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
|
||||
out vec3 vDir; // unnormalised model-space direction (normalised in frag)
|
||||
out vec2 vUV; // model-space polar UV for cloud tiling
|
||||
|
||||
void main() {
|
||||
gl_Position = (p3d_ModelViewProjectionMatrix * p3d_Vertex).xyww;
|
||||
// xyww trick forces depth to 1.0 (far clip) in NDC so sky is always behind
|
||||
// geometry — no depth write needed, but this makes the depth test pass even
|
||||
// without disabling depth write on the NodePath.
|
||||
|
||||
vDir = p3d_Vertex.xyz;
|
||||
|
||||
// Spherical UV: longitude (azimuth) on X, latitude (elevation) on Y.
|
||||
// Used for cloud layer texture-coordinate calculation in the fragment shader.
|
||||
float len = length(p3d_Vertex.xyz);
|
||||
vec3 d = p3d_Vertex.xyz / max(len, 0.001);
|
||||
float phi = atan(d.x, d.y); // azimuth [-π, π]
|
||||
float theta = asin(clamp(d.z, -1.0, 1.0)); // elevation [-π/2, π/2]
|
||||
vUV = vec2(phi / 6.28318530 + 0.5, theta / 3.14159265 + 0.5);
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// Procedural radial god-ray / sun-shaft overlay.
|
||||
//
|
||||
// Renders additive volumetric light streaks radiating from a sun or moon
|
||||
// position in screen-space UV. Designed for Panda3D additive blending on a
|
||||
// fullscreen CardMaker quad in render2dp.
|
||||
//
|
||||
// Uniforms (set by OutdoorLighting._createGodRaysOverlay / _applyProfileLive):
|
||||
// sunPos – sun/moon position in [0,1] screen UV space
|
||||
// rayColor – base tint colour of the rays (RGBA)
|
||||
// rayIntensity – master scale (0..1+, boosted during golden hour / sunset)
|
||||
// aspectRatio – window width / height (keeps rays radially symmetric)
|
||||
// time – seconds since scene start (drives subtle shimmer)
|
||||
#version 130
|
||||
|
||||
uniform vec2 sunPos;
|
||||
uniform vec4 rayColor;
|
||||
uniform float rayIntensity;
|
||||
uniform float aspectRatio;
|
||||
uniform float time;
|
||||
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
|
||||
// ── Fast hash functions (no trig) ────────────────────────────────────────────
|
||||
float hash11(float p) {
|
||||
p = fract(p * 0.1031);
|
||||
p *= p + 33.33;
|
||||
p *= p + p;
|
||||
return fract(p);
|
||||
}
|
||||
float hash12(vec2 p) {
|
||||
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||
p3 += dot(p3, p3.yzx + 33.33);
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Aspect-correct delta from sun position.
|
||||
vec2 aspect = vec2(aspectRatio, 1.0);
|
||||
vec2 delta = (uv - sunPos) * aspect;
|
||||
float dist = length(delta);
|
||||
float angle = atan(delta.y, delta.x);
|
||||
|
||||
// ── Radial streaks ───────────────────────────────────────────────────
|
||||
// 20 streaks with per-streak random angular width, stretch and shimmer.
|
||||
const int NUM_STREAKS = 20;
|
||||
float streaks = 0.0;
|
||||
for (int i = 0; i < NUM_STREAKS; i++) {
|
||||
float fi = float(i);
|
||||
|
||||
float streakAngle = fi / float(NUM_STREAKS) * 6.28318530;
|
||||
float baseWidth = 0.010 + hash11(fi * 3.71) * 0.024;
|
||||
float stretch = 2.2 + hash11(fi * 7.43) * 5.0;
|
||||
|
||||
// Each streak breathes independently at a different frequency.
|
||||
float shimmer = 1.0 + 0.07 * sin(time * (1.0 + hash11(fi) * 2.4) + fi * 1.3);
|
||||
float angularWidth = baseWidth * shimmer;
|
||||
|
||||
// Wrap angular difference to [-π, π].
|
||||
float diff = angle - streakAngle;
|
||||
diff = diff - 6.28318530 * floor((diff + 3.14159265) / 6.28318530);
|
||||
|
||||
float gaussian = exp(-(diff * diff) / (2.0 * angularWidth * angularWidth));
|
||||
float lenFade = exp(-dist * stretch);
|
||||
|
||||
streaks += gaussian * lenFade;
|
||||
}
|
||||
|
||||
// ── Diffuse halo + tight corona ──────────────────────────────────────
|
||||
float halo = exp(-dist * 8.5) * 0.70;
|
||||
float corona = pow(max(0.0, 1.0 - dist * 4.2), 3.8) * 0.40;
|
||||
|
||||
float total = streaks + halo + corona;
|
||||
|
||||
// ── Feathering ───────────────────────────────────────────────────────
|
||||
// Suppress artefacts immediately around the source and at screen edges.
|
||||
float nearFade = smoothstep(0.0, 0.05, dist);
|
||||
|
||||
vec2 edgeDist = min(sunPos, 1.0 - sunPos);
|
||||
float edgeFade = smoothstep(0.0, 0.10, min(edgeDist.x, edgeDist.y));
|
||||
|
||||
total *= nearFade * edgeFade * rayIntensity;
|
||||
|
||||
// ── Subtle chromatic fringe ──────────────────────────────────────────
|
||||
// The RGB channels are sampled at slightly offset radii, producing a
|
||||
// thin prismatic ring around the corona. It adds cinematic atmosphere
|
||||
// without being garish. The effect is strongest near the source and
|
||||
// fades with distance.
|
||||
float fringeMask = exp(-dist * 12.0) * 0.18;
|
||||
float rOffset = 0.004 * aspectRatio;
|
||||
float bOffset = -0.004 * aspectRatio;
|
||||
vec2 rDir = normalize(delta + vec2(0.001)) * rOffset;
|
||||
vec2 bDir = normalize(delta + vec2(0.001)) * bOffset;
|
||||
|
||||
// Re-evaluate total at offset positions for R and B channels.
|
||||
vec2 deltaR = (uv + rDir - sunPos) * aspect;
|
||||
float distR = length(deltaR);
|
||||
float coronaR = pow(max(0.0, 1.0 - distR * 4.2), 3.8) * 0.40;
|
||||
float haloR = exp(-distR * 8.5) * 0.70;
|
||||
|
||||
vec2 deltaB = (uv + bDir - sunPos) * aspect;
|
||||
float distB = length(deltaB);
|
||||
float coronaB = pow(max(0.0, 1.0 - distB * 4.2), 3.8) * 0.40;
|
||||
float haloB = exp(-distB * 8.5) * 0.70;
|
||||
|
||||
float fringeR = (coronaR + haloR) * nearFade * edgeFade * rayIntensity;
|
||||
float fringeB = (coronaB + haloB) * nearFade * edgeFade * rayIntensity;
|
||||
|
||||
// Blend chromatic fringe into main signal.
|
||||
vec3 colour;
|
||||
colour.r = rayColor.r * mix(total, fringeR, fringeMask);
|
||||
colour.g = rayColor.g * total;
|
||||
colour.b = rayColor.b * mix(total, fringeB, fringeMask);
|
||||
|
||||
// Premultiplied alpha for the additive blend mode.
|
||||
float alpha = (colour.r + colour.g + colour.b) / 3.0 * rayColor.a;
|
||||
|
||||
fragColor = vec4(colour, alpha);
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Fullscreen pass-through vertex shader used for god-ray and atmospheric
|
||||
// overlay effects rendered on a fullscreen CardMaker quad in render2dp.
|
||||
#version 130
|
||||
|
||||
in vec4 p3d_Vertex;
|
||||
in vec2 p3d_MultiTexCoord0;
|
||||
out vec2 uv;
|
||||
|
||||
void main() {
|
||||
gl_Position = p3d_Vertex;
|
||||
uv = p3d_MultiTexCoord0;
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
// Water surface fragment shader.
|
||||
//
|
||||
// Technique overview
|
||||
// ──────────────────
|
||||
// 1. Two-layer procedural gradient noise builds a per-pixel wave normal.
|
||||
// No texture atlas required – the entire effect is analytic.
|
||||
// 2. Fresnel equation (Schlick approximation) blends between:
|
||||
// • deep-water refraction colour (zone-specific waterColor tint)
|
||||
// • planar reflection texture sampled with distortion
|
||||
// 3. Phong specular on top gives the sun-glint / sparkle.
|
||||
// 4. A subtle edge-foam brightening is derived from world-space position
|
||||
// (no depth buffer needed – approximated by a modulated wave term).
|
||||
// 5. Alpha is 0.88 so the reflection isn't opaque and water stays readable
|
||||
// even without depth sorting.
|
||||
//
|
||||
// Uniforms set by OutdoorLighting._setupWaterNode / _tickWaterUniforms:
|
||||
// osl_ReflectionTex – planar reflection render-to-texture
|
||||
// osl_WaterColor – (r,g,b,a) zone water tint / refraction base
|
||||
// osl_SunColor – key light colour for specular
|
||||
// osl_SunDir – normalised direction *toward* the sun (world space)
|
||||
// osl_CameraPos – camera world position (for Fresnel / specular)
|
||||
// osl_Time – seconds (animation)
|
||||
// osl_WaveScale – UV tiling scale matching the vertex stage
|
||||
// osl_WaveSpeed – animation speed multiplier
|
||||
// osl_FresnelPower – Fresnel exponent (3–5 is physically plausible)
|
||||
// osl_Roughness – wave normal perturbation strength (0.2–0.6)
|
||||
// osl_ReflectionStrength – master reflection blend weight (0.0–1.0)
|
||||
#version 130
|
||||
|
||||
uniform sampler2D osl_ReflectionTex;
|
||||
uniform vec4 osl_WaterColor;
|
||||
uniform vec4 osl_SunColor;
|
||||
uniform vec3 osl_SunDir;
|
||||
uniform vec3 osl_CameraPos;
|
||||
uniform float osl_Time;
|
||||
uniform float osl_WaveScale;
|
||||
uniform float osl_WaveSpeed;
|
||||
uniform float osl_FresnelPower;
|
||||
uniform float osl_Roughness;
|
||||
uniform float osl_ReflectionStrength;
|
||||
|
||||
in vec2 vTexCoord;
|
||||
in vec3 vWorldPos;
|
||||
in vec3 vWorldNormal;
|
||||
in vec4 vClipPos;
|
||||
out vec4 fragColor;
|
||||
|
||||
// ── Gradient noise ───────────────────────────────────────────────────────────
|
||||
// Returns a value in [-1, 1] using smooth gradient noise (Perlin-like).
|
||||
|
||||
vec2 _hash2(vec2 p) {
|
||||
p = vec2(dot(p, vec2(127.1, 311.7)),
|
||||
dot(p, vec2(269.5, 183.3)));
|
||||
return -1.0 + 2.0 * fract(sin(p) * 43758.5453);
|
||||
}
|
||||
|
||||
float _noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f); // Hermite smooth step
|
||||
return mix(
|
||||
mix(dot(_hash2(i), f),
|
||||
dot(_hash2(i + vec2(1,0)), f - vec2(1,0)), u.x),
|
||||
mix(dot(_hash2(i + vec2(0,1)), f - vec2(0,1)),
|
||||
dot(_hash2(i + vec2(1,1)), f - vec2(1,1)), u.x),
|
||||
u.y
|
||||
) * 0.5 + 0.5; // remap to [0,1]
|
||||
}
|
||||
|
||||
// ── Procedural wave normal ───────────────────────────────────────────────────
|
||||
// Samples noise at two UV layers, computes gradients, and returns a
|
||||
// perturbed surface normal in world space.
|
||||
vec3 _waveNormal(vec2 baseUV, float t) {
|
||||
float scale = osl_WaveScale;
|
||||
float spd = osl_WaveSpeed;
|
||||
float rough = osl_Roughness;
|
||||
|
||||
vec2 uv1 = baseUV * scale + vec2(t * spd * 0.024, t * spd * 0.016);
|
||||
vec2 uv2 = baseUV * scale * 0.65 - vec2(t * spd * 0.019, t * spd * 0.028);
|
||||
|
||||
const float eps = 0.025;
|
||||
|
||||
// First wave layer gradient
|
||||
float h00 = _noise(uv1);
|
||||
float hdx = _noise(uv1 + vec2(eps, 0.0));
|
||||
float hdy = _noise(uv1 + vec2(0.0, eps));
|
||||
|
||||
// Second wave layer gradient
|
||||
float h00b = _noise(uv2);
|
||||
float hdxb = _noise(uv2 + vec2(eps, 0.0));
|
||||
float hdyb = _noise(uv2 + vec2(0.0, eps));
|
||||
|
||||
// Combined gradient (normalised by eps)
|
||||
vec2 grad = vec2(
|
||||
((hdx - h00) + (hdxb - h00b)),
|
||||
((hdy - h00) + (hdyb - h00b))
|
||||
) * (rough / eps);
|
||||
|
||||
// Blend gradient with the geometry's up-normal
|
||||
// We assume the water surface is approximately horizontal so vWorldNormal ≈ (0,0,1).
|
||||
// The gradient perturbs the XZ plane (in Panda3D Y-up convention that is XY).
|
||||
vec3 perturbed = normalize(vWorldNormal + vec3(-grad.x, -grad.y, 0.0));
|
||||
return perturbed;
|
||||
}
|
||||
|
||||
void main() {
|
||||
float t = osl_Time;
|
||||
|
||||
// ── Wave-perturbed normal ────────────────────────────────────────────
|
||||
vec3 N = _waveNormal(vTexCoord, t);
|
||||
|
||||
// ── View direction ───────────────────────────────────────────────────
|
||||
vec3 V = normalize(osl_CameraPos - vWorldPos);
|
||||
|
||||
// ── Fresnel (Schlick approximation) ──────────────────────────────────
|
||||
// F0 for water-air interface ≈ 0.02
|
||||
float NdotV = max(0.0, dot(N, V));
|
||||
float f0 = 0.020;
|
||||
float fresnel = f0 + (1.0 - f0) * pow(1.0 - NdotV, osl_FresnelPower);
|
||||
fresnel = clamp(fresnel, 0.0, 1.0);
|
||||
|
||||
// ── Planar reflection lookup (distorted by wave normal) ──────────────
|
||||
vec2 screenUV = (vClipPos.xy / vClipPos.w) * 0.5 + 0.5;
|
||||
// Distort reflection UV by the wave normal's XY deviation.
|
||||
vec2 distort = (N.xy - vWorldNormal.xy) * 0.055;
|
||||
vec2 reflUV = vec2(screenUV.x + distort.x,
|
||||
1.0 - screenUV.y + distort.y);
|
||||
reflUV = clamp(reflUV, 0.001, 0.999);
|
||||
vec4 reflColor = texture(osl_ReflectionTex, reflUV);
|
||||
|
||||
// ── Deep-water / refraction colour ───────────────────────────────────
|
||||
// Modulate water base colour with a subtle depth-derived darkening.
|
||||
// We approximate depth by projecting the fragment onto the vertical axis.
|
||||
float depthFade = clamp(1.0 - abs(N.z - 0.9) * 6.0, 0.0, 1.0);
|
||||
vec4 waterBase = osl_WaterColor * (0.80 + 0.20 * depthFade);
|
||||
|
||||
// ── Blend refraction and reflection ──────────────────────────────────
|
||||
vec4 surface = mix(waterBase, reflColor, fresnel * osl_ReflectionStrength);
|
||||
|
||||
// ── Sun specular (Blinn-Phong glint) ─────────────────────────────────
|
||||
// osl_SunDir points FROM the scene TOWARD the sun.
|
||||
vec3 sunToward = normalize(osl_SunDir);
|
||||
vec3 H = normalize(V + sunToward);
|
||||
float NdotH = max(0.0, dot(N, H));
|
||||
// High shininess (128) = tight glint; spread by roughness.
|
||||
float shininess = mix(256.0, 32.0, osl_Roughness);
|
||||
float spec = pow(NdotH, shininess);
|
||||
// Attenuate specular when sun is below the wave horizon.
|
||||
float NdotL = max(0.0, dot(N, sunToward));
|
||||
spec *= NdotL;
|
||||
vec3 specColor = osl_SunColor.rgb * spec * 0.55;
|
||||
|
||||
// ── Edge foam (approximated by wave crest detection) ──────────────────
|
||||
// High wave crests (large upward normal component) get a slight white tinge.
|
||||
float crestFactor = smoothstep(0.80, 1.0, N.z);
|
||||
vec3 foamColor = vec3(0.92, 0.96, 1.0);
|
||||
surface.rgb = mix(surface.rgb, foamColor, crestFactor * 0.18);
|
||||
|
||||
// ── Final composition ────────────────────────────────────────────────
|
||||
vec3 finalRGB = surface.rgb + specColor;
|
||||
|
||||
// Soft alpha: more transparent near horizonal viewing angles (grazing).
|
||||
float alpha = mix(0.72, 0.92, fresnel);
|
||||
|
||||
fragColor = vec4(finalRGB, alpha);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Water surface vertex shader.
|
||||
// Passes world-space position, surface normal, texture coords and clip-space
|
||||
// position to the fragment stage. Gentle vertex-displacement is applied to
|
||||
// break up the perfectly flat water surface and sell the wave motion.
|
||||
//
|
||||
// Uniforms (set by OutdoorLighting._setupWaterNode):
|
||||
// osl_Time – seconds since scene start (drives wave animation)
|
||||
// osl_WaveScale – UV tiling scale for procedural waves
|
||||
// osl_WaveSpeed – wave animation speed multiplier
|
||||
#version 130
|
||||
|
||||
uniform mat4 p3d_ModelViewProjectionMatrix;
|
||||
uniform mat4 p3d_ModelMatrix;
|
||||
uniform float osl_Time;
|
||||
uniform float osl_WaveScale;
|
||||
uniform float osl_WaveSpeed;
|
||||
|
||||
in vec4 p3d_Vertex;
|
||||
in vec3 p3d_Normal;
|
||||
in vec2 p3d_MultiTexCoord0;
|
||||
|
||||
out vec2 vTexCoord;
|
||||
out vec3 vWorldPos;
|
||||
out vec3 vWorldNormal;
|
||||
out vec4 vClipPos;
|
||||
|
||||
// Minimal cheap hash for vertex-level displacement (not the same as the
|
||||
// higher-quality noise used in the fragment stage).
|
||||
float vhash(vec2 p) {
|
||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
|
||||
}
|
||||
|
||||
void main() {
|
||||
float t = osl_Time * osl_WaveSpeed;
|
||||
float scale = osl_WaveScale;
|
||||
|
||||
// Two-layer vertex displacement along the surface normal.
|
||||
// Kept intentionally small (~0.15 u max) so the geometry stays close
|
||||
// to the water plane and shadow/reflection cameras are not confused.
|
||||
vec2 uv = p3d_MultiTexCoord0 * scale;
|
||||
float d1 = sin(uv.x * 6.28 + t * 1.1) * cos(uv.y * 4.71 + t * 0.9) * 0.08;
|
||||
float d2 = sin(uv.x * 3.14 - t * 0.7) * sin(uv.y * 7.85 + t * 1.3) * 0.06;
|
||||
float disp = d1 + d2;
|
||||
|
||||
vec4 displaced = p3d_Vertex + vec4(p3d_Normal * disp, 0.0);
|
||||
|
||||
vec4 worldPos4 = p3d_ModelMatrix * displaced;
|
||||
vWorldPos = worldPos4.xyz;
|
||||
vWorldNormal = normalize(mat3(p3d_ModelMatrix) * p3d_Normal);
|
||||
vTexCoord = p3d_MultiTexCoord0;
|
||||
vClipPos = p3d_ModelViewProjectionMatrix * displaced;
|
||||
gl_Position = vClipPos;
|
||||
}
|
||||
Loading…
Reference in New Issue