minecraft-renderer 0.1.74 → 0.1.76
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 +58 -58
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +423 -423
- package/package.json +1 -1
- package/src/lib/utils/skins.ts +9 -0
- package/src/three/chunkMeshManager.ts +53 -77
- package/src/three/entities.ts +11 -0
- package/src/three/globalBlockBuffer.ts +19 -0
- package/src/three/globalLegacyBuffer.ts +22 -3
- package/src/three/graphicsBackendBase.ts +1 -0
- package/src/three/graphicsBackendSingleThread.ts +1 -1
- package/src/three/signTextureCache.ts +64 -0
- package/src/three/tests/signTextureCache.test.ts +60 -39
- package/src/three/worldRendererThree.ts +14 -88
package/package.json
CHANGED
package/src/lib/utils/skins.ts
CHANGED
|
@@ -8,6 +8,15 @@ import { createCanvas, loadImageFromUrl } from '../utils'
|
|
|
8
8
|
export const stevePngUrl = stevePng
|
|
9
9
|
export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
|
|
10
10
|
|
|
11
|
+
const SKIN_TEXTURE_WIDTHS = [64, 128, 256, 512, 1024] as const
|
|
12
|
+
|
|
13
|
+
/** Minecraft skin sheets: WxW or Wx(W/2) at standard power-of-two widths. */
|
|
14
|
+
export const isLikelySkinImageSize = (width: number, height: number) => {
|
|
15
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return false
|
|
16
|
+
if (!(SKIN_TEXTURE_WIDTHS as readonly number[]).includes(width)) return false
|
|
17
|
+
return height === width || height === width / 2
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
const config = {
|
|
12
21
|
apiEnabled: true,
|
|
13
22
|
}
|
|
@@ -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 {
|
|
@@ -74,6 +72,21 @@ export interface SectionObject extends THREE.Group {
|
|
|
74
72
|
_waitingForChunkDisplay?: boolean
|
|
75
73
|
}
|
|
76
74
|
|
|
75
|
+
/** Live vs allocated stats for one global GPU buffer (faces or legacy quads). */
|
|
76
|
+
export type GlobalBufferSlotStats = {
|
|
77
|
+
used: number
|
|
78
|
+
capacity: number
|
|
79
|
+
sections: number
|
|
80
|
+
usedBytes: number
|
|
81
|
+
capacityBytes: number
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type GlobalBufferStats = {
|
|
85
|
+
shaderFaces: GlobalBufferSlotStats | null
|
|
86
|
+
legacyOpaque: GlobalBufferSlotStats | null
|
|
87
|
+
legacyBlend: GlobalBufferSlotStats | null
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
export class ChunkMeshManager {
|
|
78
91
|
private static readonly REBASE_THRESHOLD = 65536
|
|
79
92
|
|
|
@@ -1348,8 +1361,12 @@ export class ChunkMeshManager {
|
|
|
1348
1361
|
sectionObject.shaderMesh = undefined
|
|
1349
1362
|
}
|
|
1350
1363
|
delete sectionObject.deferredShaderCubes
|
|
1351
|
-
//
|
|
1364
|
+
// Release shared sign textures (refcount) before disposing meshes
|
|
1352
1365
|
if (sectionObject.signsContainer) {
|
|
1366
|
+
for (const child of sectionObject.signsContainer.children) {
|
|
1367
|
+
const sign = child as THREE.Group & { signTexture?: THREE.Texture }
|
|
1368
|
+
if (sign.signTexture) releaseSignTexture(sign.signTexture)
|
|
1369
|
+
}
|
|
1353
1370
|
this.disposeContainer(sectionObject.signsContainer, false)
|
|
1354
1371
|
}
|
|
1355
1372
|
if (sectionObject.headsContainer) {
|
|
@@ -1473,16 +1490,6 @@ export class ChunkMeshManager {
|
|
|
1473
1490
|
}
|
|
1474
1491
|
}
|
|
1475
1492
|
|
|
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
1493
|
/**
|
|
1487
1494
|
* Get mesh for section if it exists
|
|
1488
1495
|
*/
|
|
@@ -1521,6 +1528,34 @@ export class ChunkMeshManager {
|
|
|
1521
1528
|
/**
|
|
1522
1529
|
* Get pool statistics
|
|
1523
1530
|
*/
|
|
1531
|
+
getGlobalBufferStats (): GlobalBufferStats {
|
|
1532
|
+
const snapshotLegacy = (buffer: GlobalLegacyBuffer | null): GlobalBufferSlotStats | null => {
|
|
1533
|
+
if (!buffer) return null
|
|
1534
|
+
return {
|
|
1535
|
+
used: buffer.getHighWatermark(),
|
|
1536
|
+
capacity: buffer.getCapacityQuads(),
|
|
1537
|
+
sections: buffer.getSectionCount(),
|
|
1538
|
+
usedBytes: buffer.getUsedMemoryBytes(),
|
|
1539
|
+
capacityBytes: buffer.getMemoryBytes(),
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const cubes = this.globalBlockBuffer
|
|
1544
|
+
return {
|
|
1545
|
+
shaderFaces: cubes
|
|
1546
|
+
? {
|
|
1547
|
+
used: cubes.getHighWatermark(),
|
|
1548
|
+
capacity: cubes.getCapacityFaces(),
|
|
1549
|
+
sections: cubes.getSectionCount(),
|
|
1550
|
+
usedBytes: cubes.getUsedMemoryBytes(),
|
|
1551
|
+
capacityBytes: cubes.getMemoryBytes(),
|
|
1552
|
+
}
|
|
1553
|
+
: null,
|
|
1554
|
+
legacyOpaque: snapshotLegacy(this.globalLegacyBuffer),
|
|
1555
|
+
legacyBlend: snapshotLegacy(this.globalLegacyBlendBuffer),
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1524
1559
|
getStats () {
|
|
1525
1560
|
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
|
1526
1561
|
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
|
|
@@ -1923,21 +1958,12 @@ export class ChunkMeshManager {
|
|
|
1923
1958
|
}
|
|
1924
1959
|
|
|
1925
1960
|
|
|
1926
|
-
type SignTextureCacheEntry = { tex: THREE.Texture, signature: string }
|
|
1927
|
-
|
|
1928
1961
|
class SignHeadsRenderer {
|
|
1929
|
-
chunkTextures = new Map<string, { [pos: string]: SignTextureCacheEntry }>()
|
|
1930
|
-
|
|
1931
1962
|
constructor (public worldRendererThree: WorldRendererThree) {
|
|
1932
1963
|
}
|
|
1933
1964
|
|
|
1934
1965
|
dispose () {
|
|
1935
|
-
|
|
1936
|
-
for (const key of Object.keys(textures)) {
|
|
1937
|
-
textures[key]!.tex.dispose()
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
this.chunkTextures.clear()
|
|
1966
|
+
disposeAllSignTextures()
|
|
1941
1967
|
}
|
|
1942
1968
|
|
|
1943
1969
|
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
|
@@ -1981,18 +2007,10 @@ class SignHeadsRenderer {
|
|
|
1981
2007
|
}
|
|
1982
2008
|
|
|
1983
2009
|
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
|
1984
|
-
const tex = this.
|
|
2010
|
+
const tex = getSignTexture(this.worldRendererThree, blockEntity, isHanging)
|
|
1985
2011
|
|
|
1986
2012
|
if (!tex) return
|
|
1987
2013
|
|
|
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
2014
|
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
|
1997
2015
|
mesh.renderOrder = 999
|
|
1998
2016
|
|
|
@@ -2008,13 +2026,14 @@ class SignHeadsRenderer {
|
|
|
2008
2026
|
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
|
2009
2027
|
}
|
|
2010
2028
|
|
|
2011
|
-
const group = new THREE.Group()
|
|
2029
|
+
const group = new THREE.Group() as THREE.Group & { signTexture?: THREE.Texture }
|
|
2012
2030
|
group.rotation.set(
|
|
2013
2031
|
0,
|
|
2014
2032
|
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
|
2015
2033
|
0
|
|
2016
2034
|
)
|
|
2017
2035
|
group.add(mesh)
|
|
2036
|
+
group.signTexture = tex
|
|
2018
2037
|
const height = (isHanging ? 10 : 8) / 16
|
|
2019
2038
|
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
|
2020
2039
|
const textPosition = height / 2 + heightOffset
|
|
@@ -2022,47 +2041,4 @@ class SignHeadsRenderer {
|
|
|
2022
2041
|
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
|
2023
2042
|
return group
|
|
2024
2043
|
}
|
|
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
2044
|
}
|
package/src/three/entities.ts
CHANGED
|
@@ -635,6 +635,17 @@ export class Entities {
|
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
/** Local preview override: hand + player model until server skin refresh or reconnect. */
|
|
639
|
+
async applyTemporaryPlayerSkinOverride (
|
|
640
|
+
skinUrl: string,
|
|
641
|
+
entityId: string | number,
|
|
642
|
+
username?: string,
|
|
643
|
+
uuid?: string
|
|
644
|
+
) {
|
|
645
|
+
this.worldRenderer.playerStateReactive.playerSkin = skinUrl
|
|
646
|
+
await this.updatePlayerSkin(entityId, username, uuid, skinUrl, undefined)
|
|
647
|
+
}
|
|
648
|
+
|
|
638
649
|
private async loadAndApplySkin(entityId: string | number, skinUrl: string, renderEars: boolean) {
|
|
639
650
|
let playerObject = this.getPlayerObject(entityId)
|
|
640
651
|
if (!playerObject) return
|
|
@@ -35,6 +35,9 @@ const MAX_UPLOAD_FACES_PER_FRAME = 15_000 // face-indexed budget (chunksStorag
|
|
|
35
35
|
const FRAGMENTATION_THRESHOLD = 0.25
|
|
36
36
|
const EMPTY_W2 = packWord2Empty()
|
|
37
37
|
|
|
38
|
+
/** CPU bytes per instanced cube face (a_w0..a_w3). */
|
|
39
|
+
export const SHADER_CUBE_BYTES_PER_FACE = 16
|
|
40
|
+
|
|
38
41
|
type PendingMove = { key: string, oldStart: number, newStart: number, count: number }
|
|
39
42
|
|
|
40
43
|
export type GlobalBlockBufferShaderData = {
|
|
@@ -162,6 +165,22 @@ export class GlobalBlockBuffer {
|
|
|
162
165
|
return this.highWatermark
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
getCapacityFaces (): number {
|
|
169
|
+
return this.capacityFaces
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getSectionCount (): number {
|
|
173
|
+
return this.sectionSlots.size
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getMemoryBytes (): number {
|
|
177
|
+
return this.capacityFaces * SHADER_CUBE_BYTES_PER_FACE
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getUsedMemoryBytes (): number {
|
|
181
|
+
return this.highWatermark * SHADER_CUBE_BYTES_PER_FACE
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
hasPendingUploads (): boolean {
|
|
166
185
|
return this.pendingRanges.length > 0
|
|
167
186
|
}
|
|
@@ -12,6 +12,11 @@ const DEFAULT_INITIAL_CAPACITY_QUADS = 128_000
|
|
|
12
12
|
const DEFAULT_GROWTH_INCREMENT_QUADS = 128_000
|
|
13
13
|
const MAX_UPLOAD_QUADS_PER_FRAME = 5_000
|
|
14
14
|
|
|
15
|
+
/** CPU bytes per allocated quad slot (all legacy vertex/index attrs). */
|
|
16
|
+
export const LEGACY_BYTES_PER_QUAD =
|
|
17
|
+
VERTS_PER_QUAD * (FLOATS_PER_VERT * 3 + FLOATS_PER_LIGHT_VERT * 2 + FLOATS_PER_UV_VERT) * 4
|
|
18
|
+
+ INDICES_PER_QUAD * 4
|
|
19
|
+
|
|
15
20
|
export const FULL_DRAW_VISIBLE_FRACTION = 0.75
|
|
16
21
|
export const SPAN_GAP_TOLERANCE_QUADS = 256
|
|
17
22
|
export const MAX_OPAQUE_SPANS = 64
|
|
@@ -475,10 +480,24 @@ export class GlobalLegacyBuffer {
|
|
|
475
480
|
return out
|
|
476
481
|
}
|
|
477
482
|
|
|
483
|
+
getHighWatermark (): number {
|
|
484
|
+
return this.highWatermark
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
getCapacityQuads (): number {
|
|
488
|
+
return this.capacityQuads
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
getSectionCount (): number {
|
|
492
|
+
return this.sectionSlots.size
|
|
493
|
+
}
|
|
494
|
+
|
|
478
495
|
getMemoryBytes (): number {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
496
|
+
return this.capacityQuads * LEGACY_BYTES_PER_QUAD
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
getUsedMemoryBytes (): number {
|
|
500
|
+
return this.highWatermark * LEGACY_BYTES_PER_QUAD
|
|
482
501
|
}
|
|
483
502
|
|
|
484
503
|
reset (): void {
|
|
@@ -35,6 +35,7 @@ export const getBackendMethods = (worldRenderer: WorldRendererThree): any => {
|
|
|
35
35
|
playEntityAnimation: worldRenderer.entities.playAnimation.bind(worldRenderer.entities),
|
|
36
36
|
damageEntity: worldRenderer.entities.handleDamageEvent.bind(worldRenderer.entities),
|
|
37
37
|
updatePlayerSkin: worldRenderer.entities.updatePlayerSkin.bind(worldRenderer.entities),
|
|
38
|
+
applyTemporaryPlayerSkinOverride: worldRenderer.entities.applyTemporaryPlayerSkinOverride.bind(worldRenderer.entities),
|
|
38
39
|
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
|
|
39
40
|
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
|
|
40
41
|
reloadWorld: worldRenderer.reloadWorld.bind(worldRenderer),
|
|
@@ -19,7 +19,7 @@ const createGraphicsBackendSingleThread: GraphicsBackendLoader = (initOptions: G
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
createGraphicsBackendSingleThread.id = 'threejs'
|
|
22
|
-
createGraphicsBackendSingleThread.displayName = 'three.js Blocking
|
|
22
|
+
createGraphicsBackendSingleThread.displayName = 'three.js Blocking'
|
|
23
23
|
createGraphicsBackendSingleThread.description = 'Simple, old and stable main thread graphics backend providing balanced performance on top of WebGL2.'
|
|
24
24
|
|
|
25
25
|
export default createGraphicsBackendSingleThread
|
|
@@ -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
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { test, expect, vi, beforeEach } from 'vitest'
|
|
3
3
|
import * as THREE from 'three'
|
|
4
|
-
import { Vec3 } from 'vec3'
|
|
5
|
-
|
|
6
|
-
vi.mock('../entity/EntityMesh', () => ({
|
|
7
|
-
getMesh: vi.fn(),
|
|
8
|
-
}))
|
|
9
4
|
|
|
10
5
|
const renderSignMock = vi.fn()
|
|
11
6
|
vi.mock('../../sign-renderer', () => ({
|
|
@@ -16,25 +11,11 @@ vi.mock('prismarine-chat', () => ({
|
|
|
16
11
|
default: () => () => ({}),
|
|
17
12
|
}))
|
|
18
13
|
|
|
19
|
-
import {
|
|
14
|
+
import { getSignTexture, releaseSignTexture, disposeAllSignTextures } from '../signTextureCache'
|
|
20
15
|
import type { WorldRendererThree } from '../worldRendererThree'
|
|
21
16
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
const material = new THREE.MeshBasicMaterial()
|
|
25
|
-
const worldRenderer = {
|
|
26
|
-
version: '1.20',
|
|
27
|
-
shaderCubeBlocksEnabled: () => false,
|
|
28
|
-
getModule: () => undefined,
|
|
29
|
-
sceneOrigin: {
|
|
30
|
-
track: () => {},
|
|
31
|
-
removeAndUntrack: () => {},
|
|
32
|
-
removeAndUntrackAll: () => {},
|
|
33
|
-
},
|
|
34
|
-
blockEntities: {},
|
|
35
|
-
worldRendererConfig: {},
|
|
36
|
-
} as unknown as WorldRendererThree
|
|
37
|
-
return new ChunkMeshManager(worldRenderer, scene, material, 256, 1)
|
|
17
|
+
function createWorldRenderer (): WorldRendererThree {
|
|
18
|
+
return { version: '1.20' } as WorldRendererThree
|
|
38
19
|
}
|
|
39
20
|
|
|
40
21
|
function stubCanvas () {
|
|
@@ -44,40 +25,80 @@ function stubCanvas () {
|
|
|
44
25
|
beforeEach(() => {
|
|
45
26
|
renderSignMock.mockReset()
|
|
46
27
|
renderSignMock.mockImplementation(() => stubCanvas())
|
|
28
|
+
disposeAllSignTextures()
|
|
47
29
|
})
|
|
48
30
|
|
|
49
|
-
test('getSignTexture: same
|
|
50
|
-
const
|
|
51
|
-
const signHeadsRenderer = (manager as unknown as { signHeadsRenderer: { getSignTexture: Function } }).signHeadsRenderer
|
|
52
|
-
const pos = new Vec3(10, 64, 10)
|
|
31
|
+
test('getSignTexture: same content at different positions shares one texture', () => {
|
|
32
|
+
const wr = createWorldRenderer()
|
|
53
33
|
const blockEntity = { Text1: '{"text":"Hello"}' }
|
|
54
34
|
|
|
55
|
-
const tex1 =
|
|
56
|
-
const tex2 =
|
|
35
|
+
const tex1 = getSignTexture(wr, blockEntity, false)
|
|
36
|
+
const tex2 = getSignTexture(wr, { ...blockEntity }, false)
|
|
57
37
|
|
|
58
38
|
expect(tex1).toBeDefined()
|
|
59
39
|
expect(tex2).toBe(tex1)
|
|
60
40
|
expect(renderSignMock).toHaveBeenCalledTimes(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('getSignTexture: different text yields different textures', () => {
|
|
44
|
+
const wr = createWorldRenderer()
|
|
61
45
|
|
|
62
|
-
|
|
46
|
+
const tex1 = getSignTexture(wr, { Text1: '{"text":"Hello"}' }, false)!
|
|
47
|
+
const tex2 = getSignTexture(wr, { Text1: '{"text":"World"}' }, false)!
|
|
48
|
+
|
|
49
|
+
expect(tex2).not.toBe(tex1)
|
|
50
|
+
expect(renderSignMock).toHaveBeenCalledTimes(2)
|
|
63
51
|
})
|
|
64
52
|
|
|
65
|
-
test('
|
|
66
|
-
const
|
|
67
|
-
const signHeadsRenderer = (manager as unknown as { signHeadsRenderer: { getSignTexture: Function } }).signHeadsRenderer
|
|
68
|
-
const pos = new Vec3(10, 64, 10)
|
|
53
|
+
test('releaseSignTexture: partial release keeps texture in cache', () => {
|
|
54
|
+
const wr = createWorldRenderer()
|
|
69
55
|
const blockEntity = { Text1: '{"text":"Hello"}' }
|
|
70
56
|
|
|
71
|
-
const tex1 =
|
|
57
|
+
const tex1 = getSignTexture(wr, blockEntity, false)!
|
|
58
|
+
const tex2 = getSignTexture(wr, blockEntity, false)!
|
|
59
|
+
expect(tex1).toBe(tex2)
|
|
60
|
+
|
|
72
61
|
const disposeSpy = vi.spyOn(tex1, 'dispose')
|
|
62
|
+
releaseSignTexture(tex1)
|
|
63
|
+
|
|
64
|
+
expect(disposeSpy).not.toHaveBeenCalled()
|
|
65
|
+
expect(getSignTexture(wr, blockEntity, false)).toBe(tex1)
|
|
66
|
+
expect(renderSignMock).toHaveBeenCalledTimes(1)
|
|
67
|
+
})
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
const
|
|
69
|
+
test('releaseSignTexture: dispose at zero refcount removes cache entry', () => {
|
|
70
|
+
const wr = createWorldRenderer()
|
|
71
|
+
const blockEntity = { Text1: '{"text":"Hello"}' }
|
|
76
72
|
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const tex = getSignTexture(wr, blockEntity, false)!
|
|
74
|
+
const disposeSpy = vi.spyOn(tex, 'dispose')
|
|
75
|
+
|
|
76
|
+
releaseSignTexture(tex)
|
|
79
77
|
expect(disposeSpy).toHaveBeenCalledTimes(1)
|
|
78
|
+
|
|
79
|
+
const tex2 = getSignTexture(wr, blockEntity, false)!
|
|
80
|
+
expect(tex2).not.toBe(tex)
|
|
80
81
|
expect(renderSignMock).toHaveBeenCalledTimes(2)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('getSignTexture: key ignores irrelevant NBT fields', () => {
|
|
85
|
+
const wr = createWorldRenderer()
|
|
86
|
+
const base = { Text1: '{"text":"Hello"}', Color: 'black' }
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
const tex1 = getSignTexture(wr, base, false)!
|
|
89
|
+
const tex2 = getSignTexture(wr, { ...base, GlowingText: 1, is_waxed: 1 }, false)!
|
|
90
|
+
|
|
91
|
+
expect(tex2).toBe(tex1)
|
|
92
|
+
expect(renderSignMock).toHaveBeenCalledTimes(1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('getSignTexture: isHanging is part of cache key', () => {
|
|
96
|
+
const wr = createWorldRenderer()
|
|
97
|
+
const blockEntity = { Text1: '{"text":"Hello"}' }
|
|
98
|
+
|
|
99
|
+
const standing = getSignTexture(wr, blockEntity, false)!
|
|
100
|
+
const hanging = getSignTexture(wr, blockEntity, true)!
|
|
101
|
+
|
|
102
|
+
expect(hanging).not.toBe(standing)
|
|
103
|
+
expect(renderSignMock).toHaveBeenCalledTimes(2)
|
|
83
104
|
})
|