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.
- package/dist/lightOwnerWorker.js +9 -0
- package/dist/lightOwnerWorker.js.map +7 -0
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +222 -222
- package/dist/minecraft-renderer.js +105 -76
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +537 -508
- package/package.json +1 -1
- package/src/bundler/bundlePrepare.test.ts +25 -0
- package/src/bundler/bundlePrepare.ts +1 -1
- package/src/graphicsBackend/appViewer.ts +15 -0
- package/src/graphicsBackend/config.ts +8 -0
- package/src/graphicsBackend/rendererDefaultOptions.test.ts +51 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +18 -1
- package/src/graphicsBackend/rendererOptionsSync.ts +4 -3
- package/src/lib/blockEntityLightRegistry.ts +4 -0
- package/src/lib/skyLight.ts +17 -0
- package/src/lib/worldrendererCommon.lightOwner.test.ts +916 -0
- package/src/lib/worldrendererCommon.removeColumn.test.ts +16 -0
- package/src/lib/worldrendererCommon.ts +340 -67
- package/src/lighting/cache/rendererLightCache.test.ts +158 -0
- package/src/lighting/cache/rendererLightCache.ts +248 -0
- package/src/lighting/coordinator/clientLightCoordinator.test.ts +102 -0
- package/src/lighting/coordinator/clientLightCoordinator.ts +574 -0
- package/src/lighting/debug/clientLightProbe.test.ts +24 -0
- package/src/lighting/debug/clientLightProbe.ts +20 -0
- package/src/lighting/debug/clientLightTrace.test.ts +271 -0
- package/src/lighting/debug/clientLightTrace.ts +335 -0
- package/src/lighting/entity/entityLightController.test.ts +216 -0
- package/src/lighting/entity/entityLightController.ts +322 -0
- package/src/lighting/owner/clientLightOwner.test.ts +467 -0
- package/src/lighting/owner/clientLightOwner.ts +523 -0
- package/src/lighting/owner/lightOwnerHost.test.ts +78 -0
- package/src/lighting/owner/lightOwnerHost.ts +202 -0
- package/src/lighting/owner/lightOwnerWorker.ts +154 -0
- package/src/lighting/owner/updateLight1171OwnerPath.test.ts +68 -0
- package/src/lighting/owner/updateLightToOwnerEvents.ts +49 -0
- package/src/lighting/tables/lightTables1171.generated.json +10 -0
- package/src/lighting/tables/lightTables1171.test.ts +168 -0
- package/src/lighting/tables/lightTables1171.ts +59 -0
- package/src/lighting/versioning/clientLightVersions.test.ts +241 -0
- package/src/lighting/versioning/clientLightVersions.ts +203 -0
- package/src/lighting/versioning/pendingSectionFlush.test.ts +214 -0
- package/src/lighting/versioning/pendingSectionFlush.ts +123 -0
- package/src/mesher-shared/lightNibblePack.ts +26 -0
- package/src/mesher-shared/mcDataWorkerShim.cjs +9 -7
- package/src/mesher-shared/modelsGeometryCommon.test.ts +6 -1
- package/src/mesher-shared/shared.ts +48 -1
- package/src/three/chunkMeshManager.ts +26 -0
- package/src/three/entities.ts +97 -8
- package/src/three/entity/EntityMesh.ts +1 -0
- package/src/three/entity/legWalkAnimation.test.ts +154 -0
- package/src/three/entity/legWalkAnimation.ts +193 -0
- package/src/three/globalBlockBuffer.ts +94 -2
- package/src/three/globalLegacyBuffer.ts +94 -2
- package/src/three/graphicsBackendBase.ts +2 -9
- package/src/three/modules/clouds.ts +191 -0
- package/src/three/modules/index.ts +4 -0
- package/src/three/modules/sunMoon.ts +151 -0
- package/src/three/skyboxRenderer.ts +15 -8
- package/src/three/tests/clouds.test.ts +59 -0
- package/src/three/tests/globalBlockBuffer.test.ts +98 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +108 -0
- package/src/three/tests/waterFog.test.ts +25 -0
- package/src/three/waterFog.ts +28 -0
- package/src/three/worldRendererThree.ts +84 -39
- package/src/wasm-mesher/bridge/render-from-wasm.ts +46 -1
- package/src/wasm-mesher/runtime-build/wasm_mesher.d.ts +31 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher.js +180 -5
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm.d.ts +11 -0
- package/src/wasm-mesher/tests/mesherLightDependencies.test.ts +83 -0
- package/src/wasm-mesher/tests/mesherWasmColumnTickSelect.test.ts +39 -0
- package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +30 -1
- package/src/wasm-mesher/tests/mesherWasmTickParseCache.test.ts +73 -0
- package/src/wasm-mesher/tests/mesherWasmTickSchedule.test.ts +51 -0
- package/src/wasm-mesher/tests/mesherWasmTopologyPostCache.test.ts +78 -0
- package/src/wasm-mesher/worker/clientLightDarkPublication.probe.test.ts +32 -0
- package/src/wasm-mesher/worker/mesherLightDependencies.ts +43 -0
- package/src/wasm-mesher/worker/mesherWasm.ownerLightSync.test.ts +198 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +506 -96
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +1 -0
- package/src/wasm-mesher/worker/mesherWasmColumnTickSelect.ts +47 -0
- package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +16 -5
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +17 -1
- package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +10 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.test.ts +208 -0
- package/src/wasm-mesher/worker/mesherWasmLightMerge.ts +228 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.test.ts +161 -0
- package/src/wasm-mesher/worker/mesherWasmOwnerLight.ts +112 -0
- package/src/wasm-mesher/worker/mesherWasmTickParseCache.ts +45 -0
- package/src/wasm-mesher/worker/mesherWasmTickSchedule.ts +44 -0
- package/src/wasm-mesher/worker/mesherWasmTopologyPostCache.ts +63 -0
- package/src/worldView/types.ts +3 -0
- package/src/worldView/worldView.geometryReload.test.ts +68 -0
- package/src/worldView/worldView.spiral.test.ts +1 -1
- 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
|
|
91
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
this.scene.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
;(this.
|
|
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
|
+
})
|