minecraft-renderer 0.1.106 → 0.1.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/lightOwnerWorker.js +9 -0
  2. package/dist/lightOwnerWorker.js.map +7 -0
  3. package/dist/mesher.js +1 -1
  4. package/dist/mesher.js.map +2 -2
  5. package/dist/mesherWasm.js +222 -222
  6. package/dist/minecraft-renderer.js +105 -76
  7. package/dist/minecraft-renderer.js.meta.json +1 -1
  8. package/dist/threeWorker.js +537 -508
  9. package/package.json +1 -1
  10. package/src/bundler/bundlePrepare.test.ts +25 -0
  11. package/src/bundler/bundlePrepare.ts +1 -1
  12. package/src/graphicsBackend/appViewer.ts +15 -0
  13. package/src/graphicsBackend/config.ts +8 -0
  14. package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
  15. package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
  16. package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
  17. package/src/lib/blockEntityLightRegistry.ts +4 -0
  18. package/src/lib/skyLight.ts +17 -0
  19. package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
  20. package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
  21. package/src/lib/worldrendererCommon.ts +340 -67
  22. package/src/lighting/cache/rendererLightCache.test.ts +158 -0
  23. package/src/lighting/cache/rendererLightCache.ts +248 -0
  24. package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
  25. package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
  26. package/src/lighting/debug/clientLightProbe.test.ts +24 -0
  27. package/src/lighting/debug/clientLightProbe.ts +20 -0
  28. package/src/lighting/debug/clientLightTrace.test.ts +271 -0
  29. package/src/lighting/debug/clientLightTrace.ts +335 -0
  30. package/src/lighting/entity/entityLightController.test.ts +216 -0
  31. package/src/lighting/entity/entityLightController.ts +322 -0
  32. package/src/lighting/owner/clientLightOwner.test.ts +467 -0
  33. package/src/lighting/owner/clientLightOwner.ts +523 -0
  34. package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
  35. package/src/lighting/owner/lightOwnerHost.ts +202 -0
  36. package/src/lighting/owner/lightOwnerWorker.ts +154 -0
  37. package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
  38. package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
  39. package/src/lighting/tables/lightTables1171.generated.json +10 -0
  40. package/src/lighting/tables/lightTables1171.test.ts +168 -0
  41. package/src/lighting/tables/lightTables1171.ts +59 -0
  42. package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
  43. package/src/lighting/versioning/clientLightVersions.ts +203 -0
  44. package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
  45. package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
  46. package/src/mesher-shared/lightNibblePack.ts +26 -0
  47. package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
  48. package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
  49. package/src/mesher-shared/shared.ts +48 -1
  50. package/src/three/chunkMeshManager.ts +26 -0
  51. package/src/three/entities.ts +97 -8
  52. package/src/three/entity/EntityMesh.ts +1 -0
  53. package/src/three/entity/legWalkAnimation.test.ts +154 -0
  54. package/src/three/entity/legWalkAnimation.ts +193 -0
  55. package/src/three/globalBlockBuffer.ts +94 -2
  56. package/src/three/globalLegacyBuffer.ts +94 -2
  57. package/src/three/graphicsBackendBase.ts +2 -9
  58. package/src/three/modules/clouds.ts +191 -0
  59. package/src/three/modules/index.ts +4 -0
  60. package/src/three/modules/sunMoon.ts +151 -0
  61. package/src/three/skyboxRenderer.ts +15 -8
  62. package/src/three/tests/clouds.test.ts +59 -0
  63. package/src/three/tests/globalBlockBuffer.test.ts +98 -0
  64. package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
  65. package/src/three/tests/waterFog.test.ts +25 -0
  66. package/src/three/waterFog.ts +28 -0
  67. package/src/three/worldRendererThree.ts +84 -39
  68. package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
  69. package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
  70. package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
  71. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  72. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
  73. package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
  74. package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
  75. package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
  76. package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
  77. package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
  78. package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
  79. package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
  80. package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
  81. package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
  82. package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
  83. package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
  84. package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
  85. package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
  86. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
  87. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
  88. package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
  89. package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
  90. package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
  91. package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
  92. package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
  93. package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
  94. package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
  95. package/src/worldView/types.ts +3 -0
  96. package/src/worldView/worldView.geometryReload.test.ts +68 -0
  97. package/src/worldView/worldView.spiral.test.ts +1 -1
  98. package/src/worldView/worldView.ts +49 -1
@@ -87,15 +87,8 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
87
87
  },
88
88
  feedChunkPacket(payload: FeedChunkPacketPayload) {
89
89
  // Forward parsed/raw map_chunk + update_light packets from the
90
- // web-client to all WASM mesher workers. The fan-out below uses
91
- // structured clone (one Uint8Array can only be transferred to a
92
- // single recipient); useWorkerProxy still gives zero-copy transfer
93
- // from main into the off-thread renderer worker for free.
94
- const { kind, ...rest } = payload
95
- const message = { type: kind, ...rest }
96
- for (const worker of worldRenderer.workers) {
97
- worker.postMessage(message)
98
- }
90
+ // web-client to WASM mesher workers (and the light owner when flagged).
91
+ worldRenderer.feedChunkPacket(payload)
99
92
  },
100
93
  getChunksDebugState() {
101
94
  const loadedSectionsChunks: Record<string, true> = {}
@@ -0,0 +1,191 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import cloudsPng from 'mc-assets/dist/other-textures/latest/environment/clouds.png'
4
+ import type { WorldRendererThree } from '../worldRendererThree'
5
+ import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
6
+ import { vanillaCelestialAngle, vanillaDayBrightness } from '../../lib/skyLight'
7
+
8
+ // Vanilla: clouds.png is 256×256, each texel a 12×12 block cell, slab 4 blocks thick.
9
+ const CELL_SIZE = 12
10
+ const TEXTURE_CELLS = 256
11
+ const PATTERN_PERIOD = CELL_SIZE * TEXTURE_CELLS
12
+ const THICKNESS = 4
13
+ /** Vanilla drifts the pattern 0.03 blocks per tick (20 TPS) toward -X. */
14
+ const DRIFT_BLOCKS_PER_SECOND = 0.03 * 20
15
+ /** Vanilla offsets the slab bottom by +0.33 from the dimension's cloud height. */
16
+ const HEIGHT_BIAS = 0.33
17
+ const MIN_RADIUS = 256
18
+ const MAX_RADIUS = 768
19
+ const OPACITY = 0.8
20
+ /** Vanilla shades the underside darker than the top. */
21
+ const BOTTOM_SHADE = 0.7
22
+
23
+ const threeVersion = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10)
24
+
25
+ const vertexShader = /* glsl */ `
26
+ #include <common>
27
+ #include <logdepthbuf_pars_vertex>
28
+ varying vec2 vLocal;
29
+ void main() {
30
+ vLocal = (modelMatrix * vec4(position, 1.0)).xz;
31
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
32
+ #include <logdepthbuf_vertex>
33
+ }
34
+ `
35
+
36
+ const fragmentShader = /* glsl */ `
37
+ #include <common>
38
+ #include <logdepthbuf_pars_fragment>
39
+ uniform sampler2D map;
40
+ uniform vec2 patternOffset;
41
+ uniform vec3 color;
42
+ uniform float opacity;
43
+ uniform float fadeStart;
44
+ uniform float fadeEnd;
45
+ varying vec2 vLocal;
46
+ void main() {
47
+ #include <logdepthbuf_fragment>
48
+ float cover = texture2D(map, (vLocal + patternOffset) / ${PATTERN_PERIOD.toFixed(1)}).a;
49
+ if (cover < 0.5) discard;
50
+ float alpha = opacity * (1.0 - smoothstep(fadeStart, fadeEnd, length(vLocal)));
51
+ if (alpha <= 0.0) discard;
52
+ gl_FragColor = vec4(color, alpha);
53
+ #include <tonemapping_fragment>
54
+ #include <${threeVersion >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
55
+ }
56
+ `
57
+
58
+ /**
59
+ * Flat "fast graphics" clouds: one camera-following quad, the cloud mask is sampled from
60
+ * vanilla clouds.png in the fragment shader, so there is no per-cell geometry to rebuild
61
+ * as the player moves or the pattern drifts.
62
+ */
63
+ export class CloudsModule implements RendererModuleController {
64
+ private mesh?: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>
65
+ private enabled = false
66
+ private drift = 0
67
+ private timeOfDay: number
68
+ private readonly linearColor = new THREE.Color()
69
+
70
+ constructor(private readonly worldRenderer: WorldRendererThree) {
71
+ this.timeOfDay = worldRenderer.timeOfTheDay
72
+ }
73
+
74
+ enable(): void {
75
+ if (this.enabled) return
76
+ this.enabled = true
77
+ if (!this.mesh) this.create()
78
+ this.mesh!.visible = true
79
+ }
80
+
81
+ disable(): void {
82
+ if (!this.enabled) return
83
+ this.enabled = false
84
+ if (this.mesh) this.mesh.visible = false
85
+ }
86
+
87
+ autoEnableCheck(): boolean {
88
+ const cfg = this.worldRenderer.worldRendererConfig
89
+ return cfg.clouds && cfg.cardinalLight !== 'nether'
90
+ }
91
+
92
+ updateTimeOfDay(time: number): void {
93
+ this.timeOfDay = time
94
+ }
95
+
96
+ render?: (deltaTime: number) => void = deltaTime => {
97
+ this.drift = (this.drift + deltaTime * DRIFT_BLOCKS_PER_SECOND) % PATTERN_PERIOD
98
+ }
99
+
100
+ dispose(): void {
101
+ if (!this.mesh) return
102
+ this.worldRenderer.scene.remove(this.mesh)
103
+ this.mesh.geometry.dispose()
104
+ this.mesh.material.uniforms.map.value?.dispose()
105
+ this.mesh.material.dispose()
106
+ this.mesh = undefined
107
+ }
108
+
109
+ private create(): void {
110
+ const map = new THREE.TextureLoader().load(cloudsPng)
111
+ map.wrapS = THREE.RepeatWrapping
112
+ map.wrapT = THREE.RepeatWrapping
113
+ map.magFilter = THREE.NearestFilter
114
+ map.minFilter = THREE.NearestFilter
115
+ map.generateMipmaps = false
116
+ // Keep png row 0 at -Z like vanilla's texture-to-world mapping.
117
+ map.flipY = false
118
+
119
+ const material = new THREE.ShaderMaterial({
120
+ uniforms: {
121
+ map: { value: map },
122
+ patternOffset: { value: new THREE.Vector2() },
123
+ color: { value: new THREE.Color(1, 1, 1) },
124
+ opacity: { value: OPACITY },
125
+ fadeStart: { value: MIN_RADIUS * 0.55 },
126
+ fadeEnd: { value: MIN_RADIUS }
127
+ },
128
+ vertexShader,
129
+ fragmentShader,
130
+ transparent: true,
131
+ depthWrite: false,
132
+ depthTest: true,
133
+ side: THREE.DoubleSide,
134
+ forceSinglePass: true
135
+ })
136
+
137
+ const geometry = new THREE.PlaneGeometry(2, 2)
138
+ geometry.rotateX(-Math.PI / 2)
139
+
140
+ const mesh = new THREE.Mesh(geometry, material)
141
+ mesh.name = 'clouds'
142
+ mesh.frustumCulled = false
143
+ // After sun/moon (-2) and stars (-1), before other transparents so water/glass composite on top.
144
+ mesh.renderOrder = -0.5
145
+ mesh.onBeforeRender = () => this.syncToCamera(mesh)
146
+ this.mesh = mesh
147
+ this.worldRenderer.scene.add(mesh)
148
+ }
149
+
150
+ private syncToCamera(mesh: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>): void {
151
+ const { sceneOrigin, viewDistance, worldRendererConfig } = this.worldRenderer
152
+ const { uniforms } = mesh.material
153
+
154
+ const radius = THREE.MathUtils.clamp(viewDistance * 16 * 2, MIN_RADIUS, MAX_RADIUS)
155
+ uniforms.fadeStart.value = radius * 0.55
156
+ uniforms.fadeEnd.value = radius
157
+
158
+ // Show the slab bottom from below and its top from above, like a 4-block-thick cloud.
159
+ const bottom = worldRendererConfig.cloudHeight + HEIGHT_BIAS
160
+ const fromBelow = sceneOrigin.y < bottom + THICKNESS / 2
161
+ const planeY = fromBelow ? bottom : bottom + THICKNESS
162
+ mesh.position.set(0, planeY - sceneOrigin.y, 0)
163
+ mesh.scale.set(radius, 1, radius)
164
+ mesh.updateMatrixWorld()
165
+
166
+ // Wrap on the CPU in doubles so the shader never sees large world coordinates.
167
+ const wrap = (v: number) => ((v % PATTERN_PERIOD) + PATTERN_PERIOD) % PATTERN_PERIOD
168
+ uniforms.patternOffset.value.set(wrap(sceneOrigin.x + this.drift), wrap(sceneOrigin.z))
169
+
170
+ const day = vanillaDayBrightness(vanillaCelestialAngle(this.timeOfDay))
171
+ let r = day * 0.9 + 0.1
172
+ let g = day * 0.9 + 0.1
173
+ let b = day * 0.85 + 0.15
174
+ if (worldRendererConfig.isRaining) {
175
+ // Vanilla desaturates and darkens clouds toward grey while raining.
176
+ const grey = (r * 0.3 + g * 0.59 + b * 0.11) * 0.6
177
+ r = r * 0.05 + grey * 0.95
178
+ g = g * 0.05 + grey * 0.95
179
+ b = b * 0.05 + grey * 0.95
180
+ }
181
+ const shade = fromBelow ? BOTTOM_SHADE : 1
182
+ this.linearColor.setRGB(r * shade, g * shade, b * shade, THREE.SRGBColorSpace)
183
+ uniforms.color.value.copy(this.linearColor)
184
+ }
185
+ }
186
+
187
+ export const cloudsManifest: RendererModuleManifest = {
188
+ id: 'clouds',
189
+ controller: CloudsModule,
190
+ enabledDefault: true
191
+ }
@@ -1,12 +1,16 @@
1
1
  //@ts-nocheck
2
2
  import { blockBreakParticlesManifest } from './blockBreakParticles'
3
3
  import { cameraBobbingManifest } from './cameraBobbing'
4
+ import { cloudsManifest } from './clouds'
4
5
  import { rainManifest } from './rain'
5
6
  import { sciFiWorldRevealManifest } from './sciFiWorldReveal'
6
7
  import { starfieldManifest } from './starfield'
8
+ import { sunMoonManifest } from './sunMoon'
7
9
 
8
10
  export const BUILTIN_MODULES = {
9
11
  starfield: starfieldManifest,
12
+ sunMoon: sunMoonManifest,
13
+ clouds: cloudsManifest,
10
14
  futuristicReveal: sciFiWorldRevealManifest,
11
15
  rain: rainManifest,
12
16
  cameraBobbing: cameraBobbingManifest,
@@ -0,0 +1,151 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import sunPng from 'mc-assets/dist/other-textures/latest/environment/sun.png'
4
+ import moonPhasesPng from 'mc-assets/dist/other-textures/latest/environment/moon_phases.png'
5
+ import type { WorldRendererThree } from '../worldRendererThree'
6
+ import type { RendererModuleController, RendererModuleManifest } from '../rendererModuleSystem'
7
+ import { vanillaCelestialAngle } from '../../lib/skyLight'
8
+
9
+ // Vanilla geometry: sun quad ±30 and moon quad ±20, both 100 blocks from the camera.
10
+ const DISTANCE = 100
11
+ const SUN_SIZE = 60
12
+ const MOON_SIZE = 40
13
+ /** moon_phases.png is a 4×2 grid. */
14
+ const MOON_COLS = 4
15
+ const MOON_ROWS = 2
16
+
17
+ const loadPixelTexture = (url: string, onLoad?: (tex: THREE.Texture) => void): THREE.Texture => {
18
+ const tex = new THREE.TextureLoader().load(url, onLoad)
19
+ tex.colorSpace = THREE.SRGBColorSpace
20
+ tex.magFilter = THREE.NearestFilter
21
+ tex.minFilter = THREE.NearestFilter
22
+ tex.generateMipmaps = false
23
+ return tex
24
+ }
25
+
26
+ const createBodyMaterial = (map: THREE.Texture) =>
27
+ new THREE.MeshBasicMaterial({
28
+ map,
29
+ transparent: true,
30
+ // Vanilla blends celestial bodies with (SRC_ALPHA, ONE): additive over the sky colour.
31
+ blending: THREE.AdditiveBlending,
32
+ depthWrite: false,
33
+ // Terrain drawn in the opaque pass must still hide the sun/moon.
34
+ depthTest: true,
35
+ fog: false,
36
+ side: THREE.DoubleSide,
37
+ forceSinglePass: true
38
+ })
39
+
40
+ export class SunMoonModule implements RendererModuleController {
41
+ private pivot?: THREE.Group
42
+ private sun?: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial>
43
+ private moon?: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial>
44
+ private moonTexture?: THREE.Texture
45
+ private enabled = false
46
+ private timeOfDay: number
47
+ private moonPhase = 0
48
+
49
+ constructor(private readonly worldRenderer: WorldRendererThree) {
50
+ this.timeOfDay = worldRenderer.timeOfTheDay
51
+ }
52
+
53
+ enable(): void {
54
+ if (this.enabled) return
55
+ this.enabled = true
56
+ if (!this.pivot) this.create()
57
+ this.pivot!.visible = true
58
+ this.applyTime()
59
+ }
60
+
61
+ disable(): void {
62
+ if (!this.enabled) return
63
+ this.enabled = false
64
+ if (this.pivot) this.pivot.visible = false
65
+ }
66
+
67
+ autoEnableCheck(): boolean {
68
+ const cfg = this.worldRenderer.worldRendererConfig
69
+ return cfg.defaultSkybox && cfg.cardinalLight !== 'nether'
70
+ }
71
+
72
+ updateTimeOfDay(time: number): void {
73
+ this.timeOfDay = time
74
+ if (this.enabled) this.applyTime()
75
+ }
76
+
77
+ updateMoonPhase(phase: number): void {
78
+ this.moonPhase = ((Math.trunc(phase) % 8) + 8) % 8
79
+ this.applyMoonPhase()
80
+ }
81
+
82
+ dispose(): void {
83
+ if (!this.pivot) return
84
+ this.worldRenderer.scene.remove(this.pivot)
85
+ for (const mesh of [this.sun, this.moon]) {
86
+ if (!mesh) continue
87
+ mesh.geometry.dispose()
88
+ mesh.material.map?.dispose()
89
+ mesh.material.dispose()
90
+ }
91
+ this.pivot = undefined
92
+ this.sun = undefined
93
+ this.moon = undefined
94
+ this.moonTexture = undefined
95
+ }
96
+
97
+ private create(): void {
98
+ this.pivot = new THREE.Group()
99
+ this.pivot.name = 'sun-moon'
100
+
101
+ const sunGeometry = new THREE.PlaneGeometry(SUN_SIZE, SUN_SIZE)
102
+ sunGeometry.rotateX(Math.PI / 2)
103
+ this.sun = new THREE.Mesh(sunGeometry, createBodyMaterial(loadPixelTexture(sunPng)))
104
+ this.sun.position.set(0, DISTANCE, 0)
105
+
106
+ this.moonTexture = loadPixelTexture(moonPhasesPng)
107
+ this.moonTexture.repeat.set(1 / MOON_COLS, 1 / MOON_ROWS)
108
+ const moonGeometry = new THREE.PlaneGeometry(MOON_SIZE, MOON_SIZE)
109
+ moonGeometry.rotateX(-Math.PI / 2)
110
+ this.moon = new THREE.Mesh(moonGeometry, createBodyMaterial(this.moonTexture))
111
+ this.moon.position.set(0, -DISTANCE, 0)
112
+
113
+ for (const mesh of [this.sun, this.moon]) {
114
+ mesh.frustumCulled = false
115
+ // Drawn before stars (-1) and clouds so both composite on top, like vanilla.
116
+ mesh.renderOrder = -2
117
+ this.pivot.add(mesh)
118
+ }
119
+
120
+ this.applyMoonPhase()
121
+ this.worldRenderer.scene.add(this.pivot)
122
+ }
123
+
124
+ private applyMoonPhase(): void {
125
+ if (!this.moonTexture) return
126
+ const col = this.moonPhase % MOON_COLS
127
+ const row = Math.floor(this.moonPhase / MOON_COLS)
128
+ // flipY textures put row 0 (top of the png) at v = 1 - 1/rows.
129
+ this.moonTexture.offset.set(col / MOON_COLS, 1 - (row + 1) / MOON_ROWS)
130
+ }
131
+
132
+ private applyTime(): void {
133
+ if (!this.pivot || !this.sun || !this.moon) return
134
+ // Rotating +Y about Z by θ gives (-sinθ, cosθ, 0): sunrise in the east (+X),
135
+ // overhead at noon, setting in the west (-X).
136
+ const theta = vanillaCelestialAngle(this.timeOfDay) * Math.PI * 2
137
+ this.pivot.rotation.z = theta
138
+
139
+ // There is no horizon occluder on the flat sky planes, so fade bodies out as they sink
140
+ // below it instead of letting them shine through the void colour.
141
+ const sunHeight = Math.cos(theta)
142
+ this.sun.material.opacity = THREE.MathUtils.smoothstep(sunHeight, -0.15, 0.02)
143
+ this.moon.material.opacity = THREE.MathUtils.smoothstep(-sunHeight, -0.15, 0.02)
144
+ }
145
+ }
146
+
147
+ export const sunMoonManifest: RendererModuleManifest = {
148
+ id: 'sunMoon',
149
+ controller: SunMoonModule,
150
+ enabledDefault: true
151
+ }
@@ -1,6 +1,7 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
3
  import { DebugGui } from '../lib/DebugGui'
4
+ import { DEFAULT_WATER_FOG_COLOR, getWaterFogEnd, waterFogColorToRgb } from './waterFog'
4
5
 
5
6
  export const DEFAULT_TEMPERATURE = 0.75
6
7
 
@@ -17,6 +18,8 @@ export class SkyboxRenderer {
17
18
  private temperature = DEFAULT_TEMPERATURE
18
19
  private inWater = false
19
20
  private waterBreathing = false
21
+ private underwaterSeconds = 0
22
+ private readonly waterFogColor = DEFAULT_WATER_FOG_COLOR
20
23
  private fogBrightness = 0
21
24
  private prevFogBrightness = 0
22
25
  private readonly fogOrangeness = 0 // Debug property to control sky color orangeness
@@ -98,7 +101,8 @@ export class SkyboxRenderer {
98
101
  }
99
102
  }
100
103
 
101
- update(cameraPosition: THREE.Vector3, newViewDistance: number) {
104
+ update(cameraPosition: THREE.Vector3, newViewDistance: number, deltaSeconds = 0) {
105
+ if (this.inWater) this.underwaterSeconds += deltaSeconds
102
106
  if (newViewDistance !== this.viewDistance) {
103
107
  this.viewDistance = newViewDistance
104
108
  this.updateSkyColors()
@@ -139,6 +143,8 @@ export class SkyboxRenderer {
139
143
  // Update water state
140
144
  updateWaterState(inWater: boolean, waterBreathing: boolean) {
141
145
  if (this.debugGui.visible) return
146
+ // Vanilla restarts the vision ramp every time the camera leaves the water.
147
+ if (!inWater) this.underwaterSeconds = 0
142
148
  this.inWater = inWater
143
149
  this.waterBreathing = waterBreathing
144
150
  this.updateSkyColors()
@@ -342,13 +348,14 @@ export class SkyboxRenderer {
342
348
 
343
349
  // Handle water fog
344
350
  if (this.inWater) {
345
- const waterViewDistance = this.waterBreathing ? 100 : 5
346
- this.scene.fog = new THREE.Fog(new THREE.Color(0, 0, 1), 0.0025, waterViewDistance)
347
- this.scene.background = new THREE.Color(0, 0, 1)
348
-
349
- // Update sky and void colors for underwater effect
350
- ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 1))
351
- ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(0, 0, 0.6))
351
+ const fogColor = new THREE.Color(...waterFogColorToRgb(this.waterFogColor))
352
+ const fogEnd = Math.min(getWaterFogEnd(this.underwaterSeconds, this.waterBreathing), this.viewDistance * 16 * this.distanceFactor)
353
+ this.scene.fog = new THREE.Fog(fogColor, 0, fogEnd)
354
+ this.scene.background = fogColor
355
+
356
+ // Looking at the sky or void from below the surface shows the water fog colour.
357
+ ;(this.skyMesh.material as THREE.MeshBasicMaterial).color.copy(fogColor)
358
+ ;(this.voidMesh.material as THREE.MeshBasicMaterial).color.copy(fogColor)
352
359
  return
353
360
  }
354
361
 
@@ -0,0 +1,59 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+ import { afterEach, beforeEach, expect, test, vi } from 'vitest'
4
+ import { CloudsModule } from '../modules/clouds'
5
+ import { defaultWorldRendererConfig } from '../../graphicsBackend/config'
6
+
7
+ beforeEach(() => {
8
+ // No image decoding in node; the module only needs a texture object.
9
+ vi.spyOn(THREE.TextureLoader.prototype, 'load').mockImplementation(() => new THREE.Texture())
10
+ })
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks()
14
+ })
15
+
16
+ function createModule(config: Partial<typeof defaultWorldRendererConfig> = {}) {
17
+ const scene = new THREE.Scene()
18
+ const worldRenderer = {
19
+ scene,
20
+ timeOfTheDay: 6000,
21
+ viewDistance: 8,
22
+ sceneOrigin: new THREE.Vector3(),
23
+ worldRendererConfig: { ...defaultWorldRendererConfig, ...config }
24
+ }
25
+ const module = new CloudsModule(worldRenderer as any)
26
+ return { module, scene, worldRenderer }
27
+ }
28
+
29
+ test('clouds are on by default and auto mode follows the clouds option', () => {
30
+ const { module, worldRenderer } = createModule()
31
+ expect(module.autoEnableCheck()).toBe(true)
32
+ worldRenderer.worldRendererConfig.clouds = false
33
+ expect(module.autoEnableCheck()).toBe(false)
34
+ })
35
+
36
+ test('clouds stay off in the nether even when enabled', () => {
37
+ expect(createModule({ cardinalLight: 'nether' }).module.autoEnableCheck()).toBe(false)
38
+ })
39
+
40
+ test('disable hides the cloud layer and enable shows the same mesh again', () => {
41
+ const { module, scene } = createModule()
42
+ module.enable()
43
+ const mesh = scene.getObjectByName('clouds')!
44
+ expect(mesh.visible).toBe(true)
45
+
46
+ module.disable()
47
+ expect(mesh.visible).toBe(false)
48
+
49
+ module.enable()
50
+ expect(scene.getObjectByName('clouds')).toBe(mesh)
51
+ expect(mesh.visible).toBe(true)
52
+ })
53
+
54
+ test('dispose removes the cloud layer from the scene', () => {
55
+ const { module, scene } = createModule()
56
+ module.enable()
57
+ module.dispose()
58
+ expect(scene.getObjectByName('clouds')).toBeUndefined()
59
+ })
@@ -215,3 +215,101 @@ test('GlobalBlockBuffer: uploadEpoch increments when dirty range drains', () =>
215
215
  buffer.dispose()
216
216
  mat.dispose()
217
217
  })
218
+
219
+ function faceWords(faceCount: number): Uint32Array {
220
+ return new Uint32Array(faceCount * 4)
221
+ }
222
+
223
+ function drawableCubeFaces(buffer: GlobalBlockBuffer, key: string): number {
224
+ const start = buffer.getSectionDrawStart(key)
225
+ const count = buffer.getSectionDrawCount(key)
226
+ if (start === undefined || count === undefined) return 0
227
+ const spans = buildVisibleCubeSpans([{ start, count }], buffer.getHighWatermark(), false, undefined, buffer.getPendingDirtyRanges())
228
+ return spans.reduce((sum, span) => sum + span.count, 0)
229
+ }
230
+
231
+ test('GlobalBlockBuffer: compaction must not hide pending-replace displayed faces', () => {
232
+ const scene = new THREE.Scene()
233
+ const mat = createCubeBlockMaterial()
234
+ const buffer = new GlobalBlockBuffer(mat, scene)
235
+
236
+ buffer.addSection('H', faceWords(20_000), 20_000)
237
+ buffer.addSection('A', faceWords(8000), 8000)
238
+ buffer.addSection('B', faceWords(1000), 1000)
239
+ drainUploads(buffer)
240
+ const original = buffer.getSectionDrawStart('A')
241
+
242
+ buffer.addSection('A', faceWords(8000), 8000)
243
+ buffer.removeSection('H')
244
+ buffer.compactStep()
245
+ buffer.uploadDirtyRange()
246
+
247
+ expect(drawableCubeFaces(buffer, 'A')).toBe(8000)
248
+ expect(buffer.getSectionDrawStart('A')).toBe(original)
249
+ const move = buffer.getPendingMove()
250
+ expect(move?.key === 'A').toBe(false)
251
+
252
+ buffer.dispose()
253
+ mat.dispose()
254
+ })
255
+
256
+ test('GlobalBlockBuffer: 27 simultaneous remeshes keep 8000 faces on every frame', () => {
257
+ const scene = new THREE.Scene()
258
+ const mat = createCubeBlockMaterial()
259
+ const buffer = new GlobalBlockBuffer(mat, scene)
260
+ const n = 27
261
+ const faces = 8000
262
+
263
+ for (let i = 0; i < n; i++) buffer.addSection(String(i), faceWords(faces), faces)
264
+ drainUploads(buffer)
265
+ for (let i = 0; i < n; i++) buffer.addSection(String(i), faceWords(faces), faces)
266
+
267
+ for (let frame = 1; frame <= 120; frame++) {
268
+ buffer.compactStep()
269
+ buffer.uploadDirtyRange()
270
+ for (let i = 0; i < n; i++) {
271
+ expect(drawableCubeFaces(buffer, String(i)), `section ${i} on frame ${frame}`).toBe(faces)
272
+ }
273
+ if (!buffer.hasPendingUploads() && !buffer.hasPendingReplace() && !buffer.getPendingMove()) break
274
+ }
275
+
276
+ buffer.dispose()
277
+ mat.dispose()
278
+ })
279
+
280
+ test('GlobalBlockBuffer: remesh during move keeps original displayed slot', () => {
281
+ const scene = new THREE.Scene()
282
+ const mat = createCubeBlockMaterial()
283
+ const buffer = new GlobalBlockBuffer(mat, scene)
284
+
285
+ buffer.addSection('H', faceWords(20_000), 20_000)
286
+ buffer.addSection('B', faceWords(1000), 1000)
287
+ buffer.addSection('A', faceWords(8000), 8000)
288
+ drainUploads(buffer)
289
+ buffer.removeSection('H')
290
+ drainUploads(buffer)
291
+ buffer.compactStep()
292
+ expect(buffer.getPendingMove()?.key).toBe('A')
293
+ const displayed = buffer.getSectionDrawStart('A')
294
+ expect(displayed).toBeDefined()
295
+
296
+ buffer.addSection('A', faceWords(8000), 8000)
297
+ expect(buffer.getSectionDrawStart('A')).toBe(displayed)
298
+ expect(buffer.getPendingMove()).toBeNull()
299
+ expect(buffer.hasPendingReplace()).toBe(true)
300
+ expect(drawableCubeFaces(buffer, 'A')).toBe(8000)
301
+
302
+ buffer.compactStep()
303
+ buffer.uploadDirtyRange()
304
+ expect(buffer.getSectionDrawStart('A')).toBe(displayed)
305
+ expect(buffer.getPendingMove()?.key === 'A').toBe(false)
306
+ expect(drawableCubeFaces(buffer, 'A')).toBe(8000)
307
+
308
+ drainUploads(buffer)
309
+ buffer.compactStep()
310
+ finishCurrentMove(buffer)
311
+ expect(drawableCubeFaces(buffer, 'A')).toBe(8000)
312
+
313
+ buffer.dispose()
314
+ mat.dispose()
315
+ })