@@ -78,6 +78,7 @@ export class GameEngine {
7878 private islands : Islands
7979 private oceanRocks : OceanRocks
8080 private boundaryWalls : BoundaryWalls
81+ private directionalLight : THREE . DirectionalLight | null = null
8182 private aiShips : AIShips
8283 private cannonballs : Cannonballs
8384 private aiDebug : AIDebug
@@ -110,9 +111,16 @@ export class GameEngine {
110111 this . renderer . toneMapping = THREE . ACESFilmicToneMapping
111112 this . renderer . toneMappingExposure = 1.2
112113
114+ // PCSS disabled - was causing shader compilation errors
115+ // this.setupPCSS()
116+
113117 // Create scene
114118 this . scene = new THREE . Scene ( )
115119
120+ // Add atmospheric fog - matches horizon color for seamless blend
121+ // Extended far range so edges of 520-unit world aren't cut off
122+ this . scene . fog = new THREE . Fog ( '#B0E0E6' , 200 , 800 )
123+
116124 // Create camera
117125 this . camera = new THREE . PerspectiveCamera (
118126 20 , // Narrow FOV for isometric feel
@@ -176,12 +184,21 @@ export class GameEngine {
176184 this . initializeGameData ( )
177185
178186 // Add entities to scene
187+ // Render order: lower = renders first (back to front for transparency)
179188 this . scene . add ( this . sky . mesh )
189+
190+ this . ocean . seaFloor . renderOrder = 0 // Sea floor first (deepest)
191+ this . scene . add ( this . ocean . seaFloor )
192+
193+ this . islands . group . renderOrder = 1 // Islands above sea floor
194+ this . scene . add ( this . islands . group )
195+
196+ this . ocean . mesh . renderOrder = 2 // Water surface on top
180197 this . scene . add ( this . ocean . mesh )
198+
181199 this . scene . add ( this . boat . group )
182200 this . scene . add ( this . boat . getWakeMesh ( ) )
183201 this . scene . add ( this . coins . group )
184- this . scene . add ( this . islands . group )
185202 this . scene . add ( this . oceanRocks . group )
186203 this . scene . add ( this . boundaryWalls . group )
187204 this . scene . add ( this . aiShips . group )
@@ -208,15 +225,18 @@ export class GameEngine {
208225 const directional = new THREE . DirectionalLight ( '#FFF5E0' , 1.3 )
209226 directional . position . set ( 50 , 100 , 50 )
210227 directional . castShadow = true
211- directional . shadow . mapSize . set ( 2048 , 2048 )
212- directional . shadow . camera . far = 250
213- directional . shadow . camera . left = - 100
214- directional . shadow . camera . right = 100
215- directional . shadow . camera . top = 100
216- directional . shadow . camera . bottom = - 100
217- directional . shadow . bias = - 0.0001
218- directional . shadow . normalBias = 0.04
228+ directional . shadow . mapSize . set ( 4096 , 4096 )
229+ directional . shadow . camera . far = 150
230+ directional . shadow . camera . near = 20
231+ directional . shadow . camera . left = - 40
232+ directional . shadow . camera . right = 40
233+ directional . shadow . camera . top = 40
234+ directional . shadow . camera . bottom = - 40
235+ directional . shadow . bias = - 0.0003
236+ directional . shadow . normalBias = 0.02
219237 this . scene . add ( directional )
238+ this . scene . add ( directional . target )
239+ this . directionalLight = directional
220240
221241 // Fill light
222242 const fill = new THREE . DirectionalLight ( '#87CEEB' , 0.4 )
@@ -228,6 +248,91 @@ export class GameEngine {
228248 this . scene . add ( hemi )
229249 }
230250
251+ private setupPCSS ( ) : void {
252+ // PCSS - Percentage Closer Soft Shadows
253+ // Shadows are sharp near caster, soft far from caster
254+ const pcssShaderChunk = /* glsl */ `
255+ #define LIGHT_WORLD_SIZE 0.05
256+ #define LIGHT_FRUSTUM_WIDTH 80.0
257+ #define LIGHT_SIZE_UV (LIGHT_WORLD_SIZE / LIGHT_FRUSTUM_WIDTH)
258+ #define NEAR_PLANE 20.0
259+ #define NUM_SAMPLES 17
260+ #define NUM_RINGS 11
261+ #define BLOCKER_SEARCH_NUM_SAMPLES NUM_SAMPLES
262+
263+ vec2 poissonDisk[NUM_SAMPLES];
264+
265+ void initPoissonSamples(const in vec2 randomSeed) {
266+ float ANGLE_STEP = PI2 * float(NUM_RINGS) / float(NUM_SAMPLES);
267+ float INV_NUM_SAMPLES = 1.0 / float(NUM_SAMPLES);
268+ float angle = rand(randomSeed) * PI2;
269+ float radius = INV_NUM_SAMPLES;
270+ float radiusStep = radius;
271+ for (int i = 0; i < NUM_SAMPLES; i++) {
272+ poissonDisk[i] = vec2(cos(angle), sin(angle)) * pow(radius, 0.75);
273+ radius += radiusStep;
274+ angle += ANGLE_STEP;
275+ }
276+ }
277+
278+ float penumbraSize(const in float zReceiver, const in float zBlocker) {
279+ return (zReceiver - zBlocker) / zBlocker;
280+ }
281+
282+ float findBlocker(sampler2D shadowMap, const in vec2 uv, const in float zReceiver) {
283+ float searchRadius = LIGHT_SIZE_UV * (zReceiver - NEAR_PLANE) / zReceiver;
284+ float blockerDepthSum = 0.0;
285+ int numBlockers = 0;
286+ for (int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; i++) {
287+ float shadowMapDepth = unpackRGBAToDepth(texture2D(shadowMap, uv + poissonDisk[i] * searchRadius));
288+ if (shadowMapDepth < zReceiver) {
289+ blockerDepthSum += shadowMapDepth;
290+ numBlockers++;
291+ }
292+ }
293+ if (numBlockers == 0) return -1.0;
294+ return blockerDepthSum / float(numBlockers);
295+ }
296+
297+ float PCF_Filter(sampler2D shadowMap, vec2 uv, float zReceiver, float filterRadius) {
298+ float sum = 0.0;
299+ for (int i = 0; i < NUM_SAMPLES; i++) {
300+ float depth = unpackRGBAToDepth(texture2D(shadowMap, uv + poissonDisk[i] * filterRadius));
301+ if (zReceiver <= depth) sum += 1.0;
302+ }
303+ for (int i = 0; i < NUM_SAMPLES; i++) {
304+ float depth = unpackRGBAToDepth(texture2D(shadowMap, uv + -poissonDisk[i].yx * filterRadius));
305+ if (zReceiver <= depth) sum += 1.0;
306+ }
307+ return sum / (2.0 * float(NUM_SAMPLES));
308+ }
309+
310+ float PCSS(sampler2D shadowMap, vec4 coords) {
311+ vec2 uv = coords.xy;
312+ float zReceiver = coords.z;
313+ initPoissonSamples(uv);
314+ float avgBlockerDepth = findBlocker(shadowMap, uv, zReceiver);
315+ if (avgBlockerDepth == -1.0) return 1.0;
316+ float penumbraRatio = penumbraSize(zReceiver, avgBlockerDepth);
317+ float filterRadius = penumbraRatio * LIGHT_SIZE_UV * NEAR_PLANE / zReceiver;
318+ return PCF_Filter(shadowMap, uv, zReceiver, filterRadius);
319+ }
320+ `
321+
322+ // Override Three.js shadow sampling
323+ let shader = THREE . ShaderChunk . shadowmap_pars_fragment
324+ shader = shader . replace (
325+ '#ifdef USE_SHADOWMAP' ,
326+ '#ifdef USE_SHADOWMAP\n' + pcssShaderChunk ,
327+ )
328+ shader = shader . replace (
329+ '#if defined( SHADOWMAP_TYPE_PCF )' ,
330+ `return PCSS(shadowMap, shadowCoord);
331+ #if defined( SHADOWMAP_TYPE_PCF_DISABLED )` ,
332+ )
333+ THREE . ShaderChunk . shadowmap_pars_fragment = shader
334+ }
335+
231336 private initializeGameData ( ) : void {
232337 const state = useGameStore . getState ( )
233338
@@ -429,7 +534,7 @@ export class GameEngine {
429534 prevShowcaseUnlocked = true
430535
431536 // Expand world boundary for showcase zone
432- state . setWorldBoundary ( 500 )
537+ state . setWorldBoundary ( 520 )
433538
434539 // Spawn tough outer rim AIs for the showcase zone
435540 this . aiSystem . spawn ( 8 , true )
@@ -580,7 +685,8 @@ export class GameEngine {
580685 private update ( delta : number , time : number ) : void {
581686 const state = useGameStore . getState ( )
582687
583- // Always update ocean animation
688+ // Always update sky and ocean animation
689+ this . sky . update ( time )
584690 this . ocean . update ( time )
585691
586692 // Only update game systems when playing
@@ -616,6 +722,21 @@ export class GameEngine {
616722 this . boundaryWalls . update ( delta )
617723 this . aiShips . update ( time )
618724 this . cannonballs . update ( delta )
725+
726+ // Update shadow camera to follow boat for better shadow quality
727+ if ( this . directionalLight ) {
728+ this . directionalLight . position . set (
729+ boatPosition [ 0 ] + 50 ,
730+ 100 ,
731+ boatPosition [ 2 ] + 50 ,
732+ )
733+ this . directionalLight . target . position . set (
734+ boatPosition [ 0 ] ,
735+ 0 ,
736+ boatPosition [ 2 ] ,
737+ )
738+ this . directionalLight . target . updateMatrixWorld ( )
739+ }
619740 }
620741 }
621742
0 commit comments