minecraft-renderer 0.1.73 → 0.1.75
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/minecraft-renderer.js +62 -60
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +428 -426
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +4 -0
- package/src/graphicsBackend/types.ts +1 -0
- package/src/lib/bindAbortableListener.ts +1 -1
- package/src/lib/createPlayerObject.ts +1 -1
- package/src/lib/worldrendererCommon.reconfigure.test.ts +4 -1
- package/src/lib/worldrendererCommon.removeColumn.test.ts +8 -4
- package/src/lib/worldrendererCommon.ts +1 -1
- package/src/three/bannerRenderer.ts +4 -1
- package/src/three/chunkMeshManager.ts +10 -77
- package/src/three/entities.ts +2 -1
- package/src/three/graphicsBackendBase.ts +9 -5
- package/src/three/itemMesh.ts +1 -1
- package/src/three/modules/rain.ts +22 -21
- package/src/three/modules/starfield.ts +17 -5
- package/src/three/signTextureCache.ts +64 -0
- package/src/three/tests/signTextureCache.test.ts +60 -39
- package/src/three/worldRendererThree.ts +8 -90
- package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +14 -8
package/package.json
CHANGED
|
@@ -81,6 +81,10 @@ export const defaultWorldRendererConfig = {
|
|
|
81
81
|
isPlayground: false,
|
|
82
82
|
instantCameraUpdate: false,
|
|
83
83
|
isRaining: false,
|
|
84
|
+
// rainColor: 'rgb(64, 87, 148)', // original minecraft blue
|
|
85
|
+
rainColor: 'rgb(118, 148, 226)',
|
|
86
|
+
/** Rain particle opacity 0–1. */
|
|
87
|
+
rainOpacity: 0.5,
|
|
84
88
|
|
|
85
89
|
// Module states: 'enabled' = force on, 'disabled' = force off, 'auto' = use autoEnableCheck
|
|
86
90
|
moduleStates: {} as Record<string, 'enabled' | 'disabled' | 'auto'>
|
|
@@ -101,6 +101,7 @@ export interface GraphicsInitOptions<S = any> {
|
|
|
101
101
|
/** Live app options (e.g. valtio proxy); used for WebGL `gpuPreference` at context creation. */
|
|
102
102
|
getRendererOptions?: () => RendererStorageOptions
|
|
103
103
|
rendererSpecificSettings: S
|
|
104
|
+
hello?: boolean
|
|
104
105
|
callbacks: {
|
|
105
106
|
displayCriticalError: (error: Error) => void
|
|
106
107
|
setRendererSpecificSettings: (key: string, value: any) => void
|
|
@@ -10,7 +10,7 @@ import type { WorldViewWorker } from '../worldView'
|
|
|
10
10
|
export function bindAbortableListener<E extends keyof WorldViewEvents>(
|
|
11
11
|
emitter: Pick<WorldViewWorker, 'on' | 'off'>,
|
|
12
12
|
event: E,
|
|
13
|
-
handler: (...args: WorldViewEvents[E]) => void,
|
|
13
|
+
handler: (...args: Parameters<WorldViewEvents[E]>) => void,
|
|
14
14
|
signal: AbortSignal
|
|
15
15
|
): void {
|
|
16
16
|
emitter.on(event, handler as (...args: any[]) => void)
|
|
@@ -12,7 +12,7 @@ export type PlayerObjectType = PlayerObject & {
|
|
|
12
12
|
|
|
13
13
|
/** Starfield + log-depth world: cutout skin mats need alphaTest and depthWrite (not mesh traverse). */
|
|
14
14
|
export function configurePlayerSkinMaterials (playerObject: PlayerObject): void {
|
|
15
|
-
const skin = playerObject.skin
|
|
15
|
+
const skin = playerObject.skin as any
|
|
16
16
|
const materials = [
|
|
17
17
|
skin.layer1Material,
|
|
18
18
|
skin.layer1MaterialBiased,
|
|
@@ -6,6 +6,7 @@ import { proxy } from 'valtio'
|
|
|
6
6
|
import * as worldRendererModule from './worldrendererCommon'
|
|
7
7
|
import { WorldRendererCommon } from './worldrendererCommon'
|
|
8
8
|
import { defaultWorldRendererConfig } from '../graphicsBackend/config'
|
|
9
|
+
import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
|
|
9
10
|
import { getInitialPlayerState } from '../playerState/playerState'
|
|
10
11
|
import type { DisplayWorldOptions, GraphicsInitOptions } from '../graphicsBackend/types'
|
|
11
12
|
|
|
@@ -46,6 +47,8 @@ class TestWorldRenderer extends WorldRendererCommon {
|
|
|
46
47
|
updateCamera() {}
|
|
47
48
|
render() {}
|
|
48
49
|
updateShowChunksBorder() {}
|
|
50
|
+
updatePlayerEntity() {}
|
|
51
|
+
worldStop() {}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
function createRenderer(workerCount = 2, worldView?: DisplayWorldOptions['worldView']) {
|
|
@@ -56,7 +59,7 @@ function createRenderer(workerCount = 2, worldView?: DisplayWorldOptions['worldV
|
|
|
56
59
|
heightmaps: {} as Record<string, Int16Array>,
|
|
57
60
|
allChunksLoaded: false,
|
|
58
61
|
mesherWork: false,
|
|
59
|
-
instabilityFactors:
|
|
62
|
+
instabilityFactors: defaultPerformanceInstabilityFactors(),
|
|
60
63
|
intersectMedia: null,
|
|
61
64
|
},
|
|
62
65
|
renderer: '',
|
|
@@ -5,6 +5,7 @@ import { Vec3 } from 'vec3'
|
|
|
5
5
|
import { proxy } from 'valtio'
|
|
6
6
|
import { WorldRendererCommon } from './worldrendererCommon'
|
|
7
7
|
import { defaultWorldRendererConfig } from '../graphicsBackend/config'
|
|
8
|
+
import { defaultPerformanceInstabilityFactors } from '../performanceMonitor'
|
|
8
9
|
import { getInitialPlayerState } from '../playerState/playerState'
|
|
9
10
|
import type { DisplayWorldOptions, GraphicsInitOptions } from '../graphicsBackend/types'
|
|
10
11
|
|
|
@@ -45,16 +46,18 @@ class TestWorldRenderer extends WorldRendererCommon {
|
|
|
45
46
|
updateCamera() {}
|
|
46
47
|
render() {}
|
|
47
48
|
updateShowChunksBorder() {}
|
|
49
|
+
updatePlayerEntity() {}
|
|
50
|
+
worldStop() {}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
function createRenderer() {
|
|
51
54
|
const rendererState = proxy({
|
|
52
55
|
world: {
|
|
53
|
-
chunksLoaded:
|
|
54
|
-
heightmaps:
|
|
56
|
+
chunksLoaded: {} as Record<string, true>,
|
|
57
|
+
heightmaps: {} as Record<string, Int16Array>,
|
|
55
58
|
allChunksLoaded: false,
|
|
56
59
|
mesherWork: false,
|
|
57
|
-
instabilityFactors:
|
|
60
|
+
instabilityFactors: defaultPerformanceInstabilityFactors(),
|
|
58
61
|
intersectMedia: null,
|
|
59
62
|
},
|
|
60
63
|
renderer: '',
|
|
@@ -73,6 +76,7 @@ function createRenderer() {
|
|
|
73
76
|
avgRenderTime: 0,
|
|
74
77
|
world: {
|
|
75
78
|
chunksLoaded: new Set<string>(),
|
|
79
|
+
chunksLoadedCount: 0,
|
|
76
80
|
chunksTotalNumber: 0,
|
|
77
81
|
chunksFullInfo: '',
|
|
78
82
|
},
|
|
@@ -93,7 +97,7 @@ function createRenderer() {
|
|
|
93
97
|
},
|
|
94
98
|
}
|
|
95
99
|
|
|
96
|
-
const renderer = new TestWorldRenderer(displayOptions.resourcesManager, displayOptions, initOptions)
|
|
100
|
+
const renderer = new TestWorldRenderer(displayOptions.resourcesManager, displayOptions as DisplayWorldOptions, initOptions)
|
|
97
101
|
renderer.active = true
|
|
98
102
|
renderer.workers = [{ postMessage: vi.fn() }, { postMessage: vi.fn() }]
|
|
99
103
|
renderer.viewDistance = 16
|
|
@@ -1443,7 +1443,7 @@ export const initMesherWorker = (onGotMessage: (data: any) => void, workerName =
|
|
|
1443
1443
|
let mesherMcDataTintsMissingWarned = false
|
|
1444
1444
|
|
|
1445
1445
|
export const meshersSendMcData = (workers: Worker[], version: string, mcDataKeys = dynamicMcDataFiles, mcDataFull: IndexedData) => {
|
|
1446
|
-
const mcData = {
|
|
1446
|
+
const mcData: { version: IndexedData['version']; tints?: unknown; [key: string]: unknown } = {
|
|
1447
1447
|
version: JSON.parse(JSON.stringify(mcDataFull.version))
|
|
1448
1448
|
}
|
|
1449
1449
|
for (const [finalKey, sourceKey] of Object.entries(mcDataKeys)) {
|
|
@@ -270,7 +270,10 @@ export function createBannerMesh(
|
|
|
270
270
|
mesh.position.set(clothXOffset, clothYOffset, clothZPosition + thickness / 2 + 0.004)
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
const group = new THREE.Group() as THREE.Group & {
|
|
273
|
+
const group = new THREE.Group() as THREE.Group & {
|
|
274
|
+
bannerTexture?: THREE.Texture
|
|
275
|
+
bannerMaterial?: THREE.MeshBasicMaterial
|
|
276
|
+
}
|
|
274
277
|
group.rotation.set(
|
|
275
278
|
0,
|
|
276
279
|
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
|
-
import PrismarineChatLoader from 'prismarine-chat'
|
|
3
2
|
import * as THREE from 'three'
|
|
4
3
|
import * as nbt from 'prismarine-nbt'
|
|
5
4
|
import { Vec3 } from 'vec3'
|
|
@@ -20,13 +19,12 @@ import {
|
|
|
20
19
|
sectionAabbIntersectsRay,
|
|
21
20
|
type ShaderSectionRaycastEntry,
|
|
22
21
|
} from './sectionRaycastAabb'
|
|
23
|
-
import { chunkPos } from '../lib/simpleUtils'
|
|
24
|
-
import { renderSign } from '../sign-renderer'
|
|
25
22
|
import { getMesh } from './entity/EntityMesh'
|
|
26
23
|
import type { WorldRendererThree } from './worldRendererThree'
|
|
27
24
|
import { armorModel } from './entity/armorModels'
|
|
28
25
|
import { disposeObject } from './threeJsUtils'
|
|
29
26
|
import { getBannerTexture, createBannerMesh, releaseBannerTexture } from './bannerRenderer'
|
|
27
|
+
import { getSignTexture, releaseSignTexture, disposeAllSignTextures } from './signTextureCache'
|
|
30
28
|
import { BlockEntityLightRegistry } from '../lib/blockEntityLightRegistry'
|
|
31
29
|
|
|
32
30
|
export interface ChunkMeshPool {
|
|
@@ -1348,8 +1346,12 @@ export class ChunkMeshManager {
|
|
|
1348
1346
|
sectionObject.shaderMesh = undefined
|
|
1349
1347
|
}
|
|
1350
1348
|
delete sectionObject.deferredShaderCubes
|
|
1351
|
-
//
|
|
1349
|
+
// Release shared sign textures (refcount) before disposing meshes
|
|
1352
1350
|
if (sectionObject.signsContainer) {
|
|
1351
|
+
for (const child of sectionObject.signsContainer.children) {
|
|
1352
|
+
const sign = child as THREE.Group & { signTexture?: THREE.Texture }
|
|
1353
|
+
if (sign.signTexture) releaseSignTexture(sign.signTexture)
|
|
1354
|
+
}
|
|
1353
1355
|
this.disposeContainer(sectionObject.signsContainer, false)
|
|
1354
1356
|
}
|
|
1355
1357
|
if (sectionObject.headsContainer) {
|
|
@@ -1473,16 +1475,6 @@ export class ChunkMeshManager {
|
|
|
1473
1475
|
}
|
|
1474
1476
|
}
|
|
1475
1477
|
|
|
1476
|
-
/**
|
|
1477
|
-
* Forward to {@link SignHeadsRenderer.cleanChunkTextures} so callers in
|
|
1478
|
-
* `WorldRendererThree` (which historically owned the sign-texture cache)
|
|
1479
|
-
* can invalidate cached sign textures when a section is marked dirty,
|
|
1480
|
-
* without reaching into the manager's private members.
|
|
1481
|
-
*/
|
|
1482
|
-
cleanSignChunkTextures (x: number, z: number) {
|
|
1483
|
-
this.signHeadsRenderer.cleanChunkTextures(x, z)
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
1478
|
/**
|
|
1487
1479
|
* Get mesh for section if it exists
|
|
1488
1480
|
*/
|
|
@@ -1923,21 +1915,12 @@ export class ChunkMeshManager {
|
|
|
1923
1915
|
}
|
|
1924
1916
|
|
|
1925
1917
|
|
|
1926
|
-
type SignTextureCacheEntry = { tex: THREE.Texture, signature: string }
|
|
1927
|
-
|
|
1928
1918
|
class SignHeadsRenderer {
|
|
1929
|
-
chunkTextures = new Map<string, { [pos: string]: SignTextureCacheEntry }>()
|
|
1930
|
-
|
|
1931
1919
|
constructor (public worldRendererThree: WorldRendererThree) {
|
|
1932
1920
|
}
|
|
1933
1921
|
|
|
1934
1922
|
dispose () {
|
|
1935
|
-
|
|
1936
|
-
for (const key of Object.keys(textures)) {
|
|
1937
|
-
textures[key]!.tex.dispose()
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
this.chunkTextures.clear()
|
|
1923
|
+
disposeAllSignTextures()
|
|
1941
1924
|
}
|
|
1942
1925
|
|
|
1943
1926
|
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
|
@@ -1981,18 +1964,10 @@ class SignHeadsRenderer {
|
|
|
1981
1964
|
}
|
|
1982
1965
|
|
|
1983
1966
|
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
|
1984
|
-
const tex = this.
|
|
1967
|
+
const tex = getSignTexture(this.worldRendererThree, blockEntity, isHanging)
|
|
1985
1968
|
|
|
1986
1969
|
if (!tex) return
|
|
1987
1970
|
|
|
1988
|
-
// todo implement
|
|
1989
|
-
// const key = JSON.stringify({ position, rotation, isWall })
|
|
1990
|
-
// if (this.signsCache.has(key)) {
|
|
1991
|
-
// console.log('cached', key)
|
|
1992
|
-
// } else {
|
|
1993
|
-
// this.signsCache.set(key, tex)
|
|
1994
|
-
// }
|
|
1995
|
-
|
|
1996
1971
|
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
|
1997
1972
|
mesh.renderOrder = 999
|
|
1998
1973
|
|
|
@@ -2008,13 +1983,14 @@ class SignHeadsRenderer {
|
|
|
2008
1983
|
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
|
2009
1984
|
}
|
|
2010
1985
|
|
|
2011
|
-
const group = new THREE.Group()
|
|
1986
|
+
const group = new THREE.Group() as THREE.Group & { signTexture?: THREE.Texture }
|
|
2012
1987
|
group.rotation.set(
|
|
2013
1988
|
0,
|
|
2014
1989
|
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
2015
1990
|
0
|
|
2016
1991
|
)
|
|
2017
1992
|
group.add(mesh)
|
|
1993
|
+
group.signTexture = tex
|
|
2018
1994
|
const height = (isHanging ? 10 : 8) / 16
|
|
2019
1995
|
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
|
2020
1996
|
const textPosition = height / 2 + heightOffset
|
|
@@ -2022,47 +1998,4 @@ class SignHeadsRenderer {
|
|
|
2022
1998
|
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
|
2023
1999
|
return group
|
|
2024
2000
|
}
|
|
2025
|
-
|
|
2026
|
-
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
|
2027
|
-
const chunk = chunkPos(position)
|
|
2028
|
-
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
|
2029
|
-
if (!textures) {
|
|
2030
|
-
textures = {}
|
|
2031
|
-
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
|
2032
|
-
}
|
|
2033
|
-
const texturekey = `${position.x},${position.y},${position.z}`
|
|
2034
|
-
const signature = JSON.stringify(blockEntity) + '|' + isHanging + '|' + backSide
|
|
2035
|
-
const cached = textures[texturekey]
|
|
2036
|
-
if (cached && cached.signature === signature) return cached.tex
|
|
2037
|
-
|
|
2038
|
-
if (cached?.tex) cached.tex.dispose()
|
|
2039
|
-
|
|
2040
|
-
const PrismarineChat = PrismarineChatLoader(this.worldRendererThree.version)
|
|
2041
|
-
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
|
2042
|
-
if (!canvas) return
|
|
2043
|
-
const tex = new THREE.Texture(canvas)
|
|
2044
|
-
tex.magFilter = THREE.NearestFilter
|
|
2045
|
-
tex.minFilter = THREE.NearestFilter
|
|
2046
|
-
tex.needsUpdate = true
|
|
2047
|
-
textures[texturekey] = { tex, signature }
|
|
2048
|
-
return tex
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
/**
|
|
2052
|
-
* Dispose all cached sign textures for the chunk containing world coords
|
|
2053
|
-
* (x, z). Called from `WorldRendererThree.cleanChunkTextures` so that
|
|
2054
|
-
* re-meshes triggered by `setSectionDirty` (e.g. a player edits a sign)
|
|
2055
|
-
* pick up fresh block-entity NBT instead of returning the stale cached
|
|
2056
|
-
* texture from {@link SignHeadsRenderer.getSignTexture}.
|
|
2057
|
-
*/
|
|
2058
|
-
cleanChunkTextures (x: number, z: number) {
|
|
2059
|
-
const key = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
|
|
2060
|
-
const textures = this.chunkTextures.get(key)
|
|
2061
|
-
if (!textures) return
|
|
2062
|
-
const disposedKeys = Object.keys(textures)
|
|
2063
|
-
for (const k of disposedKeys) {
|
|
2064
|
-
textures[k]!.tex.dispose()
|
|
2065
|
-
delete textures[k]
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
2001
|
}
|
package/src/three/entities.ts
CHANGED
|
@@ -552,7 +552,8 @@ export class Entities {
|
|
|
552
552
|
currentSkinUrls = {} as Record<string, string>
|
|
553
553
|
|
|
554
554
|
private isCanvasBlank(canvas: HTMLCanvasElement | OffscreenCanvas): boolean {
|
|
555
|
-
|
|
555
|
+
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null
|
|
556
|
+
return !ctx
|
|
556
557
|
?.getImageData(0, 0, canvas.width, canvas.height).data
|
|
557
558
|
.some(channel => channel !== 0)
|
|
558
559
|
}
|
|
@@ -69,7 +69,7 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
|
|
|
69
69
|
// New method for updating skybox
|
|
70
70
|
setSkyboxImage: worldRenderer.skyboxRenderer.setSkyboxImage.bind(worldRenderer.skyboxRenderer),
|
|
71
71
|
// Rain methods
|
|
72
|
-
setRain:
|
|
72
|
+
setRain: worldRenderer.setRain.bind(worldRenderer),
|
|
73
73
|
spawnBlockBreakParticles(x: number, y: number, z: number, blockName: string, floorMap: number[], biomeName?: string) {
|
|
74
74
|
const module = worldRenderer.getModule<import('./modules/blockBreakParticles').BlockBreakParticlesModule>('blockBreakParticles')
|
|
75
75
|
module?.spawnBlockBreakParticles(x, y, z, blockName, floorMap, biomeName)
|
|
@@ -145,6 +145,10 @@ export const createGraphicsBackendBase = () => {
|
|
|
145
145
|
let frameTimingCollector: FrameTimingCollector | null = null
|
|
146
146
|
|
|
147
147
|
const init = (initOptionsArg: GraphicsInitOptions, mainData?: ThreeRendererMainData) => {
|
|
148
|
+
if (initOptionsArg.hello) {
|
|
149
|
+
console.log('Thanks for using minecraft-renderer project: one of the most performant Minecraft world renderers for the web!')
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
if (isWebWorker) {
|
|
149
153
|
initOptions = restoreTransferred(initOptionsArg, initOptionsRestorers, globalThis as unknown as Worker)
|
|
150
154
|
} else {
|
|
@@ -166,8 +170,8 @@ export const createGraphicsBackendBase = () => {
|
|
|
166
170
|
worldRenderer.destroy()
|
|
167
171
|
worldRenderer = null
|
|
168
172
|
frameTimingCollector = null
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
; (globalThis as any).world = undefined
|
|
174
|
+
; (globalThis as any).frameTimingCollector = undefined
|
|
171
175
|
}
|
|
172
176
|
|
|
173
177
|
if (menuBackgroundRenderer) {
|
|
@@ -202,8 +206,8 @@ export const createGraphicsBackendBase = () => {
|
|
|
202
206
|
worldRenderer.destroy()
|
|
203
207
|
worldRenderer = null
|
|
204
208
|
frameTimingCollector = null
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
; (globalThis as any).world = undefined
|
|
210
|
+
; (globalThis as any).frameTimingCollector = undefined
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
const displayOptionsRestorers = [ResourcesManager, WorldViewWorker]
|
package/src/three/itemMesh.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function create3DItemMesh (
|
|
|
40
40
|
throw new Error(`Invalid canvas dimensions: ${canvas.width}x${canvas.height}`)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const ctx = canvas.getContext('2d')
|
|
43
|
+
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
|
|
44
44
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
|
|
45
45
|
const { data } = imageData
|
|
46
46
|
|
|
@@ -17,11 +17,6 @@ const FALL_SPEED_MAX = 24
|
|
|
17
17
|
const HORIZONTAL_DRIFT = 1.2
|
|
18
18
|
const RESPAWN_BELOW = -5
|
|
19
19
|
|
|
20
|
-
const moduleOptions = {
|
|
21
|
-
particleCount: 2000,
|
|
22
|
-
speedFactor: 1,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
export class RainModule implements RendererModuleController {
|
|
26
21
|
private instancedMesh?: THREE.InstancedMesh
|
|
27
22
|
private geometry?: THREE.BoxGeometry
|
|
@@ -32,8 +27,14 @@ export class RainModule implements RendererModuleController {
|
|
|
32
27
|
private readonly tempPosition = new THREE.Vector3()
|
|
33
28
|
private readonly tempQuaternion = new THREE.Quaternion()
|
|
34
29
|
private readonly tempScale = new THREE.Vector3()
|
|
30
|
+
private readonly configUnsubs: Array<() => void> = []
|
|
35
31
|
|
|
36
|
-
constructor(private readonly worldRenderer: WorldRendererThree) {
|
|
32
|
+
constructor(private readonly worldRenderer: WorldRendererThree) {
|
|
33
|
+
this.configUnsubs.push(
|
|
34
|
+
this.worldRenderer.onReactiveConfigUpdated('rainColor', () => this.syncRainAppearance()),
|
|
35
|
+
this.worldRenderer.onReactiveConfigUpdated('rainOpacity', () => this.syncRainAppearance()),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
37
38
|
|
|
38
39
|
enable(): void {
|
|
39
40
|
if (this.enabled) return
|
|
@@ -42,6 +43,7 @@ export class RainModule implements RendererModuleController {
|
|
|
42
43
|
this.createRain()
|
|
43
44
|
} else {
|
|
44
45
|
this.instancedMesh.visible = true
|
|
46
|
+
this.syncRainAppearance()
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -60,8 +62,6 @@ export class RainModule implements RendererModuleController {
|
|
|
60
62
|
render?: (deltaTime: number) => void = (deltaTime) => {
|
|
61
63
|
if (!this.enabled || !this.instancedMesh || !this.material) return
|
|
62
64
|
|
|
63
|
-
this.syncMaterialToSceneFog()
|
|
64
|
-
|
|
65
65
|
const cameraPos = this.worldRenderer.getCameraPosition()
|
|
66
66
|
this.instancedMesh.position.set(0, 0, 0)
|
|
67
67
|
|
|
@@ -126,6 +126,9 @@ export class RainModule implements RendererModuleController {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
dispose(): void {
|
|
129
|
+
for (const unsub of this.configUnsubs) unsub()
|
|
130
|
+
this.configUnsubs.length = 0
|
|
131
|
+
|
|
129
132
|
if (this.instancedMesh) {
|
|
130
133
|
this.worldRenderer.scene.remove(this.instancedMesh)
|
|
131
134
|
}
|
|
@@ -137,33 +140,31 @@ export class RainModule implements RendererModuleController {
|
|
|
137
140
|
this.particles = []
|
|
138
141
|
}
|
|
139
142
|
|
|
140
|
-
|
|
141
|
-
private syncMaterialToSceneFog(): void {
|
|
143
|
+
private syncRainAppearance(): void {
|
|
142
144
|
if (!this.material) return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
this.material.fog = true
|
|
145
|
+
|
|
146
|
+
const { rainColor, rainOpacity } = this.worldRenderer.worldRendererConfig
|
|
147
|
+
this.material.color.set(rainColor)
|
|
148
|
+
this.material.opacity = Math.max(0, Math.min(1, rainOpacity))
|
|
149
|
+
this.material.needsUpdate = true
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
private createRain(): void {
|
|
153
|
+
const { rainColor, rainOpacity } = this.worldRenderer.worldRendererConfig
|
|
154
|
+
|
|
153
155
|
this.geometry = new THREE.BoxGeometry(0.03, 0.3, 0.03)
|
|
154
156
|
this.material = new THREE.MeshBasicMaterial({
|
|
155
|
-
color:
|
|
157
|
+
color: rainColor,
|
|
156
158
|
transparent: true,
|
|
157
|
-
opacity: 0.
|
|
159
|
+
opacity: Math.max(0, Math.min(1, rainOpacity)),
|
|
158
160
|
// Must write depth so log-depth blocks occlude rain correctly (see cubeBlockShader).
|
|
159
161
|
depthWrite: true,
|
|
160
|
-
fog:
|
|
162
|
+
fog: false,
|
|
161
163
|
})
|
|
162
164
|
|
|
163
165
|
this.instancedMesh = new THREE.InstancedMesh(this.geometry, this.material, PARTICLE_COUNT)
|
|
164
166
|
this.instancedMesh.name = 'rain-particles'
|
|
165
167
|
this.instancedMesh.frustumCulled = false
|
|
166
|
-
this.syncMaterialToSceneFog()
|
|
167
168
|
|
|
168
169
|
const dummy = new THREE.Matrix4()
|
|
169
170
|
const position = new THREE.Vector3()
|
|
@@ -26,8 +26,10 @@ class StarfieldMaterial extends THREE.ShaderMaterial {
|
|
|
26
26
|
uniform float fade;
|
|
27
27
|
varying vec3 vColor;
|
|
28
28
|
void main() {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// fade scales star brightness (0 = invisible). With additive blending this is
|
|
30
|
+
// the only way to dim stars — scene fog never reaches this shader. Driven by the
|
|
31
|
+
// rain state so stars disappear in rain like in vanilla.
|
|
32
|
+
gl_FragColor = vec4(vColor * fade, 1.0);
|
|
31
33
|
|
|
32
34
|
#include <tonemapping_fragment>
|
|
33
35
|
#include <${threeVersion >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
|
|
@@ -41,6 +43,8 @@ export class StarfieldModule implements RendererModuleController {
|
|
|
41
43
|
private timer = new THREE.Timer()
|
|
42
44
|
private enabled = false
|
|
43
45
|
private currentTime?: number
|
|
46
|
+
/** Current star brightness multiplier; lerps toward 0 while raining, 1 otherwise. */
|
|
47
|
+
private fade = 1
|
|
44
48
|
|
|
45
49
|
constructor(private readonly worldRenderer: WorldRendererThree) { }
|
|
46
50
|
|
|
@@ -72,12 +76,20 @@ export class StarfieldModule implements RendererModuleController {
|
|
|
72
76
|
return this.currentTime > nightTime && this.currentTime < morningStart
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
render?: (deltaTime: number) => void = (
|
|
79
|
+
render?: (deltaTime: number) => void = (deltaTime) => {
|
|
76
80
|
if (!this.points) return
|
|
77
81
|
this.points.position.set(0, 0, 0)
|
|
82
|
+
|
|
83
|
+
const material = this.points.material as StarfieldMaterial
|
|
78
84
|
this.timer.update(performance.now())
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
material.uniforms.time.value = this.timer.getElapsed() * 0.2
|
|
86
|
+
|
|
87
|
+
// Fade stars out while raining (vanilla scales star brightness by 1 - rainLevel).
|
|
88
|
+
// isRaining is a boolean here, so ease toward the target instead of snapping.
|
|
89
|
+
const target = this.worldRenderer.worldRendererConfig.isRaining ? 0 : 1
|
|
90
|
+
const t = Math.min(1, deltaTime * 2)
|
|
91
|
+
this.fade += (target - this.fade) * t
|
|
92
|
+
material.uniforms.fade.value = this.fade
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import PrismarineChatLoader from 'prismarine-chat'
|
|
4
|
+
import { renderSign } from '../sign-renderer'
|
|
5
|
+
import type { WorldRendererThree } from './worldRendererThree'
|
|
6
|
+
|
|
7
|
+
const signTextureCache = new Map<string, { texture: THREE.Texture, refCount: number }>()
|
|
8
|
+
|
|
9
|
+
// Build the key ONLY from fields that change rendered pixels, so two signs
|
|
10
|
+
// with identical visible text share a texture even if other NBT differs.
|
|
11
|
+
function createSignCacheKey (blockEntity: any, isHanging: boolean, backSide: boolean): string {
|
|
12
|
+
let lines: string[]
|
|
13
|
+
let color: string
|
|
14
|
+
if (blockEntity && 'front_text' in blockEntity) { // 1.20+
|
|
15
|
+
lines = blockEntity.front_text?.messages ?? []
|
|
16
|
+
color = blockEntity.front_text?.color || 'black'
|
|
17
|
+
} else { // legacy
|
|
18
|
+
lines = [blockEntity?.Text1, blockEntity?.Text2, blockEntity?.Text3, blockEntity?.Text4]
|
|
19
|
+
color = blockEntity?.Color || 'black'
|
|
20
|
+
}
|
|
21
|
+
// \0 separator: cannot appear in JSON text components, so no key collisions
|
|
22
|
+
return `${isHanging ? 1 : 0}|${backSide ? 1 : 0}|${color}|${lines.join('\0')}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getSignTexture (
|
|
26
|
+
worldRenderer: WorldRendererThree,
|
|
27
|
+
blockEntity: any,
|
|
28
|
+
isHanging: boolean,
|
|
29
|
+
backSide = false
|
|
30
|
+
): THREE.Texture | undefined {
|
|
31
|
+
const cacheKey = createSignCacheKey(blockEntity, isHanging, backSide)
|
|
32
|
+
const cached = signTextureCache.get(cacheKey)
|
|
33
|
+
if (cached) {
|
|
34
|
+
cached.refCount++
|
|
35
|
+
return cached.texture
|
|
36
|
+
}
|
|
37
|
+
const PrismarineChat = PrismarineChatLoader(worldRenderer.version)
|
|
38
|
+
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
|
39
|
+
if (!canvas) return undefined
|
|
40
|
+
const tex = new THREE.Texture(canvas)
|
|
41
|
+
tex.magFilter = THREE.NearestFilter
|
|
42
|
+
tex.minFilter = THREE.NearestFilter
|
|
43
|
+
tex.needsUpdate = true
|
|
44
|
+
signTextureCache.set(cacheKey, { texture: tex, refCount: 1 })
|
|
45
|
+
return tex
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function releaseSignTexture (texture: THREE.Texture): void {
|
|
49
|
+
for (const [key, cached] of signTextureCache.entries()) {
|
|
50
|
+
if (cached.texture === texture) {
|
|
51
|
+
cached.refCount--
|
|
52
|
+
if (cached.refCount <= 0) {
|
|
53
|
+
cached.texture.dispose()
|
|
54
|
+
signTextureCache.delete(key)
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function disposeAllSignTextures (): void {
|
|
62
|
+
for (const [, cached] of signTextureCache) cached.texture.dispose()
|
|
63
|
+
signTextureCache.clear()
|
|
64
|
+
}
|