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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- /** Starfield + log-depth world: cutout skin mats need alphaTest and depthWrite (not mesh traverse). */
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 = true
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: {
@@ -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'
@@ -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
- const verts = this.capacityQuads * VERTS_PER_QUAD
480
- return verts * (FLOATS_PER_VERT * 3 + FLOATS_PER_LIGHT_VERT * 2 + FLOATS_PER_UV_VERT) * 4
481
- + this.capacityQuads * INDICES_PER_QUAD * 4
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 Beta'
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
- text += `F: ${formatCompact(this.tilesRendered)} `
762
- text += `B: ${formatCompact(this.blocksRendered)} `
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