minecraft-renderer 0.1.75 → 0.1.77
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 +59 -56
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +395 -392
- package/package.json +1 -1
- package/src/lib/createPlayerObject.test.ts +46 -0
- package/src/lib/createPlayerObject.ts +27 -2
- package/src/lib/utils/skins.ts +9 -0
- package/src/three/chunkMeshManager.ts +43 -0
- 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/worldRendererThree.ts +13 -4
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
vi.mock('./utils/skins', () => ({
|
|
5
|
+
stevePngUrl: '',
|
|
6
|
+
loadSkinImage: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
import { PlayerObject } from 'skinview3d'
|
|
10
|
+
import { configurePlayerSkinMaterials } from './createPlayerObject'
|
|
11
|
+
|
|
12
|
+
describe('configurePlayerSkinMaterials', () => {
|
|
13
|
+
it('configures cutout materials and log-depth bias on arm/leg mats only', () => {
|
|
14
|
+
const playerObject = new PlayerObject()
|
|
15
|
+
const skin = playerObject.skin as any
|
|
16
|
+
|
|
17
|
+
configurePlayerSkinMaterials(playerObject)
|
|
18
|
+
|
|
19
|
+
const allMaterials = [
|
|
20
|
+
skin.layer1Material,
|
|
21
|
+
skin.layer1MaterialBiased,
|
|
22
|
+
skin.layer2Material,
|
|
23
|
+
skin.layer2MaterialBiased,
|
|
24
|
+
]
|
|
25
|
+
for (const mat of allMaterials) {
|
|
26
|
+
expect(mat.transparent).toBe(false)
|
|
27
|
+
expect(mat.alphaTest).toBe(0.1)
|
|
28
|
+
expect(mat.depthWrite).toBe(true)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
expect(skin.layer1MaterialBiased.userData.logDepthBiasApplied).toBe(true)
|
|
32
|
+
expect(skin.layer2MaterialBiased.userData.logDepthBiasApplied).toBe(true)
|
|
33
|
+
expect(typeof skin.layer1MaterialBiased.onBeforeCompile).toBe('function')
|
|
34
|
+
expect(typeof skin.layer2MaterialBiased.onBeforeCompile).toBe('function')
|
|
35
|
+
expect(skin.layer1MaterialBiased.onBeforeCompile).toBe(skin.layer2MaterialBiased.onBeforeCompile)
|
|
36
|
+
|
|
37
|
+
expect(skin.layer1Material.userData.logDepthBiasApplied).toBeUndefined()
|
|
38
|
+
expect(skin.layer2Material.userData.logDepthBiasApplied).toBeUndefined()
|
|
39
|
+
expect(skin.layer1Material.onBeforeCompile).not.toBe(skin.layer1MaterialBiased.onBeforeCompile)
|
|
40
|
+
expect(skin.layer2Material.onBeforeCompile).not.toBe(skin.layer2MaterialBiased.onBeforeCompile)
|
|
41
|
+
|
|
42
|
+
const firstCompile = skin.layer1MaterialBiased.onBeforeCompile
|
|
43
|
+
configurePlayerSkinMaterials(playerObject)
|
|
44
|
+
expect(skin.layer1MaterialBiased.onBeforeCompile).toBe(firstCompile)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -10,7 +10,30 @@ export type PlayerObjectType = PlayerObject & {
|
|
|
10
10
|
realUsername: string
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const LOG_DEPTH_BIAS = -2e-4 // tune visually; negative ≈ polygonOffset “closer”
|
|
14
|
+
|
|
15
|
+
function patchLogDepthBiasShader (shader: THREE.WebGLProgramParametersWithUniforms): void {
|
|
16
|
+
shader.uniforms.uLogDepthBias = { value: LOG_DEPTH_BIAS }
|
|
17
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
18
|
+
'#include <logdepthbuf_pars_fragment>',
|
|
19
|
+
`#include <logdepthbuf_pars_fragment>\nuniform float uLogDepthBias;`
|
|
20
|
+
)
|
|
21
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
22
|
+
'#include <logdepthbuf_fragment>',
|
|
23
|
+
`#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )
|
|
24
|
+
gl_FragDepth = log2( vFragDepth ) * logDepthBufFC * 0.5 + uLogDepthBias;
|
|
25
|
+
#endif`
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyLogDepthBias (material: THREE.Material): void {
|
|
30
|
+
if (material.userData.logDepthBiasApplied) return
|
|
31
|
+
material.userData.logDepthBiasApplied = true
|
|
32
|
+
material.onBeforeCompile = patchLogDepthBiasShader
|
|
33
|
+
material.needsUpdate = true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Log-depth world: opaque cutout mats (alphaTest + depthWrite, not transparent sort). */
|
|
14
37
|
export function configurePlayerSkinMaterials (playerObject: PlayerObject): void {
|
|
15
38
|
const skin = playerObject.skin as any
|
|
16
39
|
const materials = [
|
|
@@ -20,10 +43,12 @@ export function configurePlayerSkinMaterials (playerObject: PlayerObject): void
|
|
|
20
43
|
skin.layer2MaterialBiased,
|
|
21
44
|
]
|
|
22
45
|
for (const mat of materials) {
|
|
23
|
-
mat.transparent =
|
|
46
|
+
mat.transparent = false
|
|
24
47
|
mat.alphaTest = 0.1
|
|
25
48
|
mat.depthWrite = true
|
|
26
49
|
}
|
|
50
|
+
applyLogDepthBias(skin.layer1MaterialBiased)
|
|
51
|
+
applyLogDepthBias(skin.layer2MaterialBiased)
|
|
27
52
|
}
|
|
28
53
|
|
|
29
54
|
export function createPlayerObject (options: {
|
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
|
}
|
|
@@ -72,6 +72,21 @@ export interface SectionObject extends THREE.Group {
|
|
|
72
72
|
_waitingForChunkDisplay?: boolean
|
|
73
73
|
}
|
|
74
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
|
+
|
|
75
90
|
export class ChunkMeshManager {
|
|
76
91
|
private static readonly REBASE_THRESHOLD = 65536
|
|
77
92
|
|
|
@@ -1513,6 +1528,34 @@ export class ChunkMeshManager {
|
|
|
1513
1528
|
/**
|
|
1514
1529
|
* Get pool statistics
|
|
1515
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
|
+
|
|
1516
1559
|
getStats () {
|
|
1517
1560
|
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
|
|
1518
1561
|
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
|
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
|
|
@@ -758,11 +758,20 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
758
758
|
const formatCompact = (num: number) => new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(num)
|
|
759
759
|
let text = ''
|
|
760
760
|
text += `TE: ${formatFull(this.renderer.info.memory.textures)} `
|
|
761
|
-
|
|
762
|
-
|
|
761
|
+
const gb = this.chunkMeshManager.getGlobalBufferStats()
|
|
762
|
+
if (gb.shaderFaces) {
|
|
763
|
+
const s = gb.shaderFaces
|
|
764
|
+
text += `CUBE: ${formatCompact(s.used)}/${formatCompact(s.capacity)}f `
|
|
765
|
+
}
|
|
766
|
+
if (gb.legacyOpaque) {
|
|
767
|
+
const s = gb.legacyOpaque
|
|
768
|
+
text += `LEG-O: ${formatCompact(s.used)}/${formatCompact(s.capacity)}q `
|
|
769
|
+
}
|
|
770
|
+
if (gb.legacyBlend) {
|
|
771
|
+
const s = gb.legacyBlend
|
|
772
|
+
text += `LEG-B: ${formatCompact(s.used)}/${formatCompact(s.capacity)}q `
|
|
773
|
+
}
|
|
763
774
|
text += `MEM: ${this.chunkMeshManager.getEstimatedMemoryUsage().total} `
|
|
764
|
-
const poolStats = this.chunkMeshManager.getStats()
|
|
765
|
-
text += `POOL: ${poolStats.activeCount}/${poolStats.poolSize} `
|
|
766
775
|
const pf = formatPerformanceFactorsDebug(this.reactiveState.world.instabilityFactors)
|
|
767
776
|
if (pf) text += `PF: ${pf} `
|
|
768
777
|
// entities can be seen in F3
|