@@ -686,6 +686,7 @@ const renderer = new THREE.WebGLRenderer({ antialias: false });
686686renderer . setSize ( 320 , 320 , false ) ;
687687renderer . setPixelRatio ( 1 ) ;
688688renderer . shadowMap . enabled = true ;
689+ renderer . shadowMap . type = THREE . PCFSoftShadowMap ;
689690canvasWrap . appendChild ( renderer . domElement ) ;
690691
691692const scene = new THREE . Scene ( ) ;
@@ -697,6 +698,17 @@ camera.lookAt(0, 1.2, 0);
697698
698699const sunLight = new THREE . DirectionalLight ( 0xffffff , 1 ) ;
699700sunLight . position . set ( 3 , 2 , 4 ) ;
701+ sunLight . castShadow = true ;
702+ sunLight . shadow . mapSize . set ( 1536 , 1536 ) ;
703+ sunLight . shadow . radius = 1.6 ;
704+ sunLight . shadow . bias = - 0.0002 ;
705+ sunLight . shadow . normalBias = 0.02 ;
706+ sunLight . shadow . camera . near = 0.5 ;
707+ sunLight . shadow . camera . far = 120 ;
708+ sunLight . shadow . camera . left = - 10 ;
709+ sunLight . shadow . camera . right = 10 ;
710+ sunLight . shadow . camera . top = 8 ;
711+ sunLight . shadow . camera . bottom = - 8 ;
700712const sunTarget = new THREE . Object3D ( ) ;
701713sunTarget . position . set ( 0 , 0 , 0 ) ;
702714sunLight . target = sunTarget ;
@@ -796,21 +808,46 @@ const updateGlobalLightingFromUtc = (utcMs: number) => {
796808 - Math . cos ( azimuthRad ) * Math . cos ( elevationRad )
797809 ) . normalize ( ) ;
798810
799- const distanceScale = 12 ;
800- sunLight . position . copy ( sunDirection ) . multiplyScalar ( distanceScale ) ;
801- sunTarget . position . set ( 0 , 0 , 0 ) ;
811+ const interiorCenterX = ( interiorMinX + interiorMaxX ) * 0.5 ;
812+ const interiorCenterZ = ( interiorMinZ + interiorMaxZ ) * 0.5 ;
813+ const shadowTarget = new THREE . Vector3 ( interiorCenterX , playerPosition . y - playerEyeHeightM + 1.15 , interiorCenterZ ) ;
814+ const distanceScale = 40 ;
815+ sunTarget . position . copy ( shadowTarget ) ;
816+ sunLight . position . copy ( shadowTarget ) . addScaledVector ( sunDirection , distanceScale ) ;
817+
818+ const cameraToFloorDistance = Math . max ( 1 , Math . abs ( camera . position . y - shadowTarget . y ) ) ;
819+ const viewHalfHeightAtFloor = Math . tan ( ( camera . fov * DEG_TO_RAD ) / 2 ) * cameraToFloorDistance ;
820+ const viewHalfWidthAtFloor = viewHalfHeightAtFloor * Math . max ( 1 , camera . aspect ) ;
821+ const interiorHalfWidth = Math . max ( 2.5 , ( interiorMaxX - interiorMinX ) * 0.5 + 1.8 ) ;
822+ const interiorHalfDepth = Math . max ( 4.5 , ( interiorMaxZ - interiorMinZ ) * 0.5 + 2.4 ) ;
823+ const interiorRadius = Math . hypot ( interiorHalfWidth , interiorHalfDepth ) ;
824+ const visibleRadius = Math . hypot ( viewHalfWidthAtFloor + 6.0 , viewHalfHeightAtFloor + 14.0 ) ;
825+ const shadowRadius = Math . max ( interiorRadius , visibleRadius ) ;
826+ sunLight . shadow . camera . left = - shadowRadius ;
827+ sunLight . shadow . camera . right = shadowRadius ;
828+ sunLight . shadow . camera . top = shadowRadius ;
829+ sunLight . shadow . camera . bottom = - shadowRadius ;
830+ sunLight . shadow . camera . near = 0.5 ;
831+ sunLight . shadow . camera . far = Math . max ( 180 , distanceScale + ( shadowRadius * 2.4 ) ) ;
832+ sunLight . shadow . camera . updateProjectionMatrix ( ) ;
802833
803834 const distanceFactor = 1 / Math . max ( 0.25 , simulation . planetDistanceAu ) ** 2 ;
804- const dayFactor = clampNumber ( ( elevationDeg + 8 ) / 68 , 0 , 1 ) ;
805- sunLight . intensity = distanceFactor * ( 0.02 + dayFactor * 1.15 ) ;
806- ambientLight . intensity = distanceFactor * ( 0.06 + dayFactor * 0.32 ) ;
807-
808- const warmFactor = clampNumber ( ( 18 - elevationDeg ) / 30 , 0 , 1 ) ;
809- sunLight . color . setRGB ( 1 , 1 - warmFactor * 0.18 , 1 - warmFactor * 0.38 ) ;
835+ const sunAboveFactor = clampNumber ( Math . sin ( Math . max ( 0 , elevationRad ) ) , 0 , 1 ) ;
836+ const belowHorizonTaper = elevationDeg >= 0
837+ ? 1
838+ : clampNumber ( 1 - ( ( - elevationDeg ) / 4.5 ) , 0 , 1 ) ;
839+ const twilightTail = elevationDeg < 0 ? 0.06 * belowHorizonTaper : 0 ;
840+ const directSunFactor = ( sunAboveFactor * belowHorizonTaper ) + twilightTail ;
841+ sunLight . intensity = distanceFactor * directSunFactor * 2.45 ;
842+ ambientLight . intensity = distanceFactor * ( 0.03 + directSunFactor * 0.31 ) ;
843+ sunLight . castShadow = directSunFactor > 0.015 ;
844+
845+ const warmFactor = clampNumber ( ( 9 - elevationDeg ) / 14 , 0 , 1 ) ;
846+ sunLight . color . setRGB ( 1 , 1 - warmFactor * 0.28 , 1 - warmFactor * 0.58 ) ;
810847 ambientLight . color . setRGB (
811- 0.72 + dayFactor * 0.28 ,
812- 0.75 + dayFactor * 0.25 ,
813- 0.84 + dayFactor * 0.16
848+ 0.60 + directSunFactor * 0.36 ,
849+ 0.63 + directSunFactor * 0.34 ,
850+ 0.72 + directSunFactor * 0.24
814851 ) ;
815852} ;
816853
@@ -979,6 +1016,9 @@ const createModuleMesh = (moduleDoc: GeneratedModule): THREE.Group => {
9791016 : new THREE . CylinderGeometry ( block . radiusTop , block . radiusBottom , block . height , block . radialSegments ) ;
9801017 const material = getMaterial ( block ) ;
9811018 const mesh = new THREE . Mesh ( geometry , material ) ;
1019+ const isWindow = block . role . includes ( 'window' ) ;
1020+ mesh . receiveShadow = ! isWindow ;
1021+ mesh . castShadow = ! isWindow ;
9821022 mesh . name = `${ moduleDoc . id } :${ block . id } ` ;
9831023 if ( block . role === 'furniture-paper-a4' ) {
9841024 mesh . userData . interactionKind = 'captains-letter-paper' ;
@@ -1178,8 +1218,8 @@ const getActiveLevelOffset = (): number => {
11781218 let best = levels [ 0 ] ?? 0 ;
11791219 let bestDistance = Number . POSITIVE_INFINITY ;
11801220 for ( const level of levels ) {
1181- const levelY = level * ladderDeckLevelHeightM ;
1182- const distance = Math . abs ( playerPosition . y - levelY ) ;
1221+ const standingEyeY = ( level * ladderDeckLevelHeightM ) + playerEyeHeightM ;
1222+ const distance = Math . abs ( playerPosition . y - standingEyeY ) ;
11831223 if ( distance < bestDistance ) {
11841224 bestDistance = distance ;
11851225 best = level ;
@@ -1672,7 +1712,8 @@ const alignPlayerToClimbVolume = (volume: ModuleVolume, deltaSeconds: number) =>
16721712 const halfY = sy / 2 ;
16731713 const levelOffset = getClimbVolumeLevelOffset ( volume ) ;
16741714 const hasBelow = levelOffset !== null ? ladderLevelOffsets . includes ( levelOffset - 1 ) : false ;
1675- const minY = Math . max ( cy - halfY + 0.05 , hasBelow ? - Infinity : playerEyeHeightM ) ;
1715+ const levelStandingEyeY = ( ( levelOffset ?? 0 ) * ladderDeckLevelHeightM ) + playerEyeHeightM ;
1716+ const minY = Math . max ( cy - halfY + 0.05 , hasBelow ? - Infinity : levelStandingEyeY ) ;
16761717 const maxY = cy + halfY - 0.05 ;
16771718
16781719 const targetX = cx + clamp ( activeClimbOffsetX , - ( ( sx / 2 ) - 0.03 ) , ( sx / 2 ) - 0.03 ) ;
@@ -2647,6 +2688,10 @@ const mapModuleIndexAfterTopologyMutation = (oldIndex: number, mutation: Topolog
26472688 return oldIndex > mutation . index ? oldIndex - 1 : oldIndex ;
26482689 }
26492690
2691+ if ( mutation . kind === 'ladder-level' ) {
2692+ return null ;
2693+ }
2694+
26502695 return oldIndex ;
26512696} ;
26522697
0 commit comments