minecraft-renderer 0.1.95 → 0.1.97
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/mesher.js.map +2 -2
- package/dist/mesherWasm.js +369 -196
- package/dist/minecraft-renderer.js +75 -76
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +420 -421
- package/package.json +1 -1
- package/src/mesher-shared/exportedGeometryTypes.ts +3 -1
- package/src/mesher-shared/shaderCubeFormat.ts +3 -0
- package/src/mesher-shared/shared.ts +2 -1
- package/src/three/entities.ts +133 -22
- package/src/three/graphicsBackendBase.ts +1 -0
- package/src/three/shaders/cubeBlockShader.ts +75 -14
- package/src/three/worldGeometryExport.ts +18 -3
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +83 -30
- package/src/wasm-mesher/tests/shaderCubeUvParity.test.ts +226 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
/** Shared geometry export shapes (worker bridge + main-thread viewer). */
|
|
3
3
|
|
|
4
|
+
import { SHADER_CUBES_FORMAT_VERSION } from './shaderCubeFormat'
|
|
5
|
+
|
|
4
6
|
export interface ExportedSection {
|
|
5
7
|
key: string
|
|
6
8
|
position: { x: number; y: number; z: number }
|
|
@@ -25,7 +27,7 @@ export interface ExportedSection {
|
|
|
25
27
|
shaderCubes?: {
|
|
26
28
|
words: Uint32Array
|
|
27
29
|
count: number
|
|
28
|
-
formatVersion:
|
|
30
|
+
formatVersion: typeof SHADER_CUBES_FORMAT_VERSION
|
|
29
31
|
}
|
|
30
32
|
visibilitySet?: number
|
|
31
33
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { BlockType } from '../playground/shared'
|
|
3
|
+
import { SHADER_CUBES_FORMAT_VERSION } from './shaderCubeFormat'
|
|
3
4
|
|
|
4
5
|
export const SECTION_HEIGHT = 16
|
|
5
6
|
|
|
@@ -80,7 +81,7 @@ export type MesherGeometryOutput = {
|
|
|
80
81
|
shaderCubes?: {
|
|
81
82
|
words: Uint32Array
|
|
82
83
|
count: number
|
|
83
|
-
formatVersion:
|
|
84
|
+
formatVersion: typeof SHADER_CUBES_FORMAT_VERSION
|
|
84
85
|
}
|
|
85
86
|
/** Packed VisibilitySet (36 bits) for section occlusion / smart cull. */
|
|
86
87
|
visibilitySet?: number
|
package/src/three/entities.ts
CHANGED
|
@@ -145,41 +145,116 @@ function shortestYawRadians(fromYawRad: number, toYawRad: number): number {
|
|
|
145
145
|
return norm > Math.PI ? norm - TAU_YAW : norm
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Voice chat presence for a player, drawn as a small mic glyph before their nametag. */
|
|
149
|
+
export type PlayerVoiceState = 'speaking' | 'muted' | 'idle'
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Draws a small mic glyph centered at the origin of the current transform.
|
|
153
|
+
* `size` is the glyph's bounding box (both width and height).
|
|
154
|
+
*/
|
|
155
|
+
function drawVoiceStateIcon(ctx: OffscreenCanvasRenderingContext2D, state: PlayerVoiceState, cx: number, cy: number, size: number) {
|
|
156
|
+
const color = state === 'speaking' ? '#3ddc47' : state === 'muted' ? '#ff5555' : '#cfcfcf'
|
|
157
|
+
|
|
158
|
+
ctx.save()
|
|
159
|
+
ctx.translate(cx, cy)
|
|
160
|
+
|
|
161
|
+
if (state === 'speaking') {
|
|
162
|
+
ctx.beginPath()
|
|
163
|
+
ctx.arc(0, 0, size * 0.62, 0, Math.PI * 2)
|
|
164
|
+
ctx.strokeStyle = 'rgba(61, 220, 71, 0.55)'
|
|
165
|
+
ctx.lineWidth = Math.max(1, size * 0.08)
|
|
166
|
+
ctx.stroke()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ctx.fillStyle = color
|
|
170
|
+
ctx.strokeStyle = color
|
|
171
|
+
ctx.lineWidth = Math.max(1, size * 0.12)
|
|
172
|
+
|
|
173
|
+
// Mic body: a rounded capsule
|
|
174
|
+
const bodyWidth = size * 0.34
|
|
175
|
+
const bodyHeight = size * 0.55
|
|
176
|
+
const bodyRadius = bodyWidth / 2
|
|
177
|
+
ctx.beginPath()
|
|
178
|
+
ctx.moveTo(-bodyRadius, -bodyHeight / 2 + bodyRadius)
|
|
179
|
+
ctx.arcTo(-bodyRadius, -bodyHeight / 2, 0, -bodyHeight / 2, bodyRadius)
|
|
180
|
+
ctx.arcTo(bodyRadius, -bodyHeight / 2, bodyRadius, -bodyHeight / 2 + bodyRadius, bodyRadius)
|
|
181
|
+
ctx.lineTo(bodyRadius, bodyHeight / 2 - bodyRadius)
|
|
182
|
+
ctx.arcTo(bodyRadius, bodyHeight / 2, 0, bodyHeight / 2, bodyRadius)
|
|
183
|
+
ctx.arcTo(-bodyRadius, bodyHeight / 2, -bodyRadius, bodyHeight / 2 - bodyRadius, bodyRadius)
|
|
184
|
+
ctx.closePath()
|
|
185
|
+
ctx.fill()
|
|
186
|
+
|
|
187
|
+
// Stand: arc + base line
|
|
188
|
+
ctx.beginPath()
|
|
189
|
+
ctx.arc(0, bodyHeight / 2 - bodyRadius / 2, size * 0.32, Math.PI * 0.15, Math.PI * 0.85, false)
|
|
190
|
+
ctx.stroke()
|
|
191
|
+
ctx.beginPath()
|
|
192
|
+
ctx.moveTo(0, size * 0.5)
|
|
193
|
+
ctx.lineTo(0, size * 0.62)
|
|
194
|
+
ctx.stroke()
|
|
195
|
+
|
|
196
|
+
if (state === 'muted') {
|
|
197
|
+
ctx.beginPath()
|
|
198
|
+
ctx.moveTo(-size * 0.42, -size * 0.42)
|
|
199
|
+
ctx.lineTo(size * 0.42, size * 0.42)
|
|
200
|
+
ctx.stroke()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ctx.restore()
|
|
204
|
+
}
|
|
205
|
+
|
|
148
206
|
function getUsernameTexture(
|
|
149
|
-
{ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255 }: any,
|
|
207
|
+
{ username, nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)', nameTagTextOpacity = 255, voiceState }: any,
|
|
150
208
|
{ fontFamily = 'mojangles' }: any,
|
|
151
209
|
version: string
|
|
152
210
|
) {
|
|
153
|
-
const
|
|
154
|
-
|
|
211
|
+
const measureCanvas = createCanvas(64, 64)
|
|
155
212
|
const PrismarineChat = PrismarineChatLoader(version)
|
|
156
213
|
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
214
|
+
const measureCtx = measureCanvas.getContext('2d')
|
|
215
|
+
if (!measureCtx) throw new Error('Could not get 2d context')
|
|
159
216
|
|
|
160
217
|
const fontSize = 48
|
|
161
218
|
const padding = 5
|
|
162
|
-
|
|
219
|
+
measureCtx.font = `${fontSize}px ${fontFamily}`
|
|
163
220
|
|
|
164
221
|
const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n')
|
|
165
222
|
let textWidth = 0
|
|
166
223
|
for (const line of plainLines) {
|
|
167
|
-
const width =
|
|
224
|
+
const width = measureCtx.measureText(line).width + padding * 2
|
|
168
225
|
if (width > textWidth) textWidth = width
|
|
169
226
|
}
|
|
170
227
|
|
|
171
|
-
|
|
172
|
-
canvas.height = (fontSize + padding) * plainLines.length
|
|
228
|
+
const height = (fontSize + padding) * plainLines.length
|
|
173
229
|
|
|
174
|
-
|
|
175
|
-
|
|
230
|
+
// Render the text on its own canvas first so the existing centered-text
|
|
231
|
+
// layout in renderComponent() is unaffected by the icon we add alongside it.
|
|
232
|
+
const textCanvas = createCanvas(textWidth, height)
|
|
233
|
+
const textCtx = textCanvas.getContext('2d')
|
|
234
|
+
if (!textCtx) throw new Error('Could not get 2d context')
|
|
176
235
|
|
|
177
|
-
|
|
236
|
+
textCtx.fillStyle = nameTagBackgroundColor
|
|
237
|
+
textCtx.fillRect(0, 0, textCanvas.width, textCanvas.height)
|
|
238
|
+
textCtx.globalAlpha = nameTagTextOpacity / 255
|
|
178
239
|
|
|
179
|
-
const textRendered = renderComponent(username, PrismarineChat,
|
|
240
|
+
const textRendered = renderComponent(username, PrismarineChat, textCanvas, fontSize, 'white', -padding + fontSize)
|
|
180
241
|
if (!textRendered) return undefined
|
|
181
242
|
|
|
182
|
-
|
|
243
|
+
textCtx.globalAlpha = 1
|
|
244
|
+
|
|
245
|
+
if (!voiceState) return textCanvas
|
|
246
|
+
|
|
247
|
+
const iconSize = fontSize * 0.55
|
|
248
|
+
const iconAreaWidth = iconSize + padding * 2
|
|
249
|
+
|
|
250
|
+
const canvas = createCanvas(iconAreaWidth + textWidth, height)
|
|
251
|
+
const ctx = canvas.getContext('2d')
|
|
252
|
+
if (!ctx) throw new Error('Could not get 2d context')
|
|
253
|
+
|
|
254
|
+
ctx.fillStyle = nameTagBackgroundColor
|
|
255
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
256
|
+
drawVoiceStateIcon(ctx, voiceState, iconAreaWidth / 2, canvas.height / 2, iconSize)
|
|
257
|
+
ctx.drawImage(textCanvas, iconAreaWidth, 0)
|
|
183
258
|
|
|
184
259
|
return canvas
|
|
185
260
|
}
|
|
@@ -234,6 +309,13 @@ const addNametag = (entity, options: { fontFamily: string }, mesh, version: stri
|
|
|
234
309
|
return nameTag
|
|
235
310
|
}
|
|
236
311
|
|
|
312
|
+
/** Player nametags are placed above the skin model and scaled up; text_display/mob nametags are not. */
|
|
313
|
+
function applyPlayerNametagTransform(nametag: THREE.Object3D | undefined, playerObject: { position: THREE.Vector3; scale: THREE.Vector3 }) {
|
|
314
|
+
if (!nametag) return
|
|
315
|
+
nametag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
|
316
|
+
nametag.scale.multiplyScalar(12)
|
|
317
|
+
}
|
|
318
|
+
|
|
237
319
|
// todo cleanup
|
|
238
320
|
const nametags = {}
|
|
239
321
|
|
|
@@ -395,6 +477,7 @@ export class Entities {
|
|
|
395
477
|
}
|
|
396
478
|
this.entities = {}
|
|
397
479
|
this.currentSkinUrls = {}
|
|
480
|
+
this.playerVoiceStates = {}
|
|
398
481
|
|
|
399
482
|
// Clean up player entity
|
|
400
483
|
if (this.playerEntity) {
|
|
@@ -447,9 +530,7 @@ export class Entities {
|
|
|
447
530
|
}
|
|
448
531
|
|
|
449
532
|
private setSceneAttached(attached: boolean, entity: THREE.Object3D | null = null): void {
|
|
450
|
-
const targets = entity
|
|
451
|
-
? [entity]
|
|
452
|
-
: [...Object.values(this.entities), ...(this.playerEntity ? [this.playerEntity] : [])]
|
|
533
|
+
const targets = entity ? [entity] : [...Object.values(this.entities), ...(this.playerEntity ? [this.playerEntity] : [])]
|
|
453
534
|
for (const ent of targets) {
|
|
454
535
|
if (attached) {
|
|
455
536
|
if (!this.worldRenderer.scene.children.includes(ent)) this.worldRenderer.scene.add(ent)
|
|
@@ -739,6 +820,33 @@ export class Entities {
|
|
|
739
820
|
return playerObject
|
|
740
821
|
}
|
|
741
822
|
|
|
823
|
+
/** Current voice chat presence per entity id, keyed as a string. Absent = no voice indicator. */
|
|
824
|
+
playerVoiceStates: Record<string, PlayerVoiceState> = {}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Show or hide the mic glyph on a player's nametag. `state` undefined hides it
|
|
828
|
+
* (e.g. the player left voice chat, or walked out of proximity range).
|
|
829
|
+
* Regenerates the nametag texture, so callers should only invoke this on
|
|
830
|
+
* actual state transitions (speaking start/stop, mute toggle), not per-frame.
|
|
831
|
+
*/
|
|
832
|
+
setPlayerVoiceState(entityId: string | number, username: string, state: PlayerVoiceState | undefined) {
|
|
833
|
+
const key = String(entityId)
|
|
834
|
+
if (state) {
|
|
835
|
+
this.playerVoiceStates[key] = state
|
|
836
|
+
} else {
|
|
837
|
+
delete this.playerVoiceStates[key]
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const e = this.entities[entityId]
|
|
841
|
+
const playerObject = e?.playerObject
|
|
842
|
+
if (!e || !playerObject) return
|
|
843
|
+
const mesh = e.children.find(c => c.name === 'mesh')
|
|
844
|
+
if (!mesh) return
|
|
845
|
+
|
|
846
|
+
const nametag = addNametag({ username, voiceState: state }, this.entitiesOptions, mesh, this.worldRenderer.version)
|
|
847
|
+
applyPlayerNametagTransform(nametag, playerObject)
|
|
848
|
+
}
|
|
849
|
+
|
|
742
850
|
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string; capeUrl?: string }>
|
|
743
851
|
currentSkinUrls = {} as Record<string, string>
|
|
744
852
|
|
|
@@ -1089,6 +1197,7 @@ export class Entities {
|
|
|
1089
1197
|
disposeObject(e)
|
|
1090
1198
|
// todo dispose textures as well ?
|
|
1091
1199
|
delete this.entities[entity.id]
|
|
1200
|
+
delete this.playerVoiceStates[String(entity.id)]
|
|
1092
1201
|
return
|
|
1093
1202
|
}
|
|
1094
1203
|
if (!this.isRenderingAllowed()) return
|
|
@@ -1160,11 +1269,13 @@ export class Entities {
|
|
|
1160
1269
|
mesh = wrapper
|
|
1161
1270
|
|
|
1162
1271
|
if (entity.username) {
|
|
1163
|
-
const nametag = addNametag(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1272
|
+
const nametag = addNametag(
|
|
1273
|
+
{ ...entity, voiceState: this.playerVoiceStates[entity.id] },
|
|
1274
|
+
{ fontFamily: 'mojangles' },
|
|
1275
|
+
wrapper,
|
|
1276
|
+
this.worldRenderer.version
|
|
1277
|
+
)
|
|
1278
|
+
applyPlayerNametagTransform(nametag, playerObject)
|
|
1168
1279
|
}
|
|
1169
1280
|
} else {
|
|
1170
1281
|
mesh = getEntityMesh(this.mcData, entity, this.worldRenderer, this.entitiesOptions, { ...overrides, customModel: entity['customModel'] })
|
|
@@ -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
|
+
setPlayerVoiceState: worldRenderer.entities.setPlayerVoiceState.bind(worldRenderer.entities),
|
|
38
39
|
applyTemporaryPlayerSkinOverride: worldRenderer.entities.applyTemporaryPlayerSkinOverride.bind(worldRenderer.entities),
|
|
39
40
|
changeHandSwingingState: worldRenderer.changeHandSwingingState.bind(worldRenderer),
|
|
40
41
|
getHighestBlocks: worldRenderer.getHighestBlocks.bind(worldRenderer),
|
|
@@ -7,6 +7,64 @@ export type { BlockLightmapParams }
|
|
|
7
7
|
// Face order: UP=0, DOWN=1, EAST=2, WEST=3, SOUTH=4, NORTH=5
|
|
8
8
|
// matches WASM mesher face order (mesher.rs FACE_NAMES)
|
|
9
9
|
|
|
10
|
+
/** Term of tile-local UV in quad-corner coordinates (u, v ∈ {0,1}). */
|
|
11
|
+
type FaceUvTerm = 'u' | '1-u' | 'v' | '1-v'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Canonical tile-local UV per face for su>0, sv>0. Matches legacy elemFaces
|
|
15
|
+
* (+ implicit +180° on down; see render-from-wasm.ts `r += 180`).
|
|
16
|
+
* Index = faceId: UP, DOWN, EAST, WEST, SOUTH, NORTH.
|
|
17
|
+
* Single source of truth — GLSL and shaderCubeFaceUv() are generated from this.
|
|
18
|
+
*/
|
|
19
|
+
export const FACE_UV_TERMS: ReadonlyArray<readonly [FaceUvTerm, FaceUvTerm]> = [
|
|
20
|
+
['u', '1-v'], // UP
|
|
21
|
+
['1-u', 'v'], // DOWN
|
|
22
|
+
['v', 'u'], // EAST
|
|
23
|
+
['v', 'u'], // WEST
|
|
24
|
+
['u', 'v'], // SOUTH
|
|
25
|
+
['1-u', 'v'] // NORTH
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const glslTerm = (t: FaceUvTerm) =>
|
|
29
|
+
({
|
|
30
|
+
u: 'u',
|
|
31
|
+
v: 'v',
|
|
32
|
+
'1-u': '1.0 - u',
|
|
33
|
+
'1-v': '1.0 - v'
|
|
34
|
+
})[t]
|
|
35
|
+
|
|
36
|
+
/** GLSL if/else-if chain for per-face UV — must stay in sync with FACE_UV_TERMS. */
|
|
37
|
+
export function buildFaceUvGlsl(): string {
|
|
38
|
+
return FACE_UV_TERMS.map(([ut, vt], i) => ` ${i === 0 ? 'if' : 'else if'} (faceId == ${i}u) uv = vec2(${glslTerm(ut)}, ${glslTerm(vt)});`).join('\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function evalFaceUvTerm(t: FaceUvTerm, cu: number, cv: number): number {
|
|
42
|
+
switch (t) {
|
|
43
|
+
case 'u':
|
|
44
|
+
return cu
|
|
45
|
+
case '1-u':
|
|
46
|
+
return 1 - cu
|
|
47
|
+
case 'v':
|
|
48
|
+
return cv
|
|
49
|
+
case '1-v':
|
|
50
|
+
return 1 - cv
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** vi — quad corner in shader order (0..3): u = vi & 1, v = (vi >> 1) & 1. */
|
|
55
|
+
export function shaderCubeFaceUv(faceId: number, vi: number, flipU: boolean, flipV: boolean): [number, number] {
|
|
56
|
+
const [ut, vt] = FACE_UV_TERMS[faceId] ?? FACE_UV_TERMS[0]
|
|
57
|
+
const cu = vi & 1
|
|
58
|
+
const cv = (vi >> 1) & 1
|
|
59
|
+
let uLocal = evalFaceUvTerm(ut, cu, cv)
|
|
60
|
+
let vLocal = evalFaceUvTerm(vt, cu, cv)
|
|
61
|
+
if (flipU) uLocal = 1 - uLocal
|
|
62
|
+
if (flipV) vLocal = 1 - vLocal
|
|
63
|
+
return [uLocal, vLocal]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const faceUvGlsl = buildFaceUvGlsl()
|
|
67
|
+
|
|
10
68
|
const vertexShader = /* glsl */ `
|
|
11
69
|
precision highp float;
|
|
12
70
|
precision highp int;
|
|
@@ -143,18 +201,16 @@ void main() {
|
|
|
143
201
|
v_tintIndex = int(tint);
|
|
144
202
|
v_faceId = int(faceId);
|
|
145
203
|
|
|
146
|
-
// --- Per-face UV
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
v_uv = vec2(1.0 - u, v);
|
|
157
|
-
}
|
|
204
|
+
// --- Per-face UV: canonical tile-local UV (su>0, sv>0), generated from
|
|
205
|
+
// FACE_UV_TERMS — same source as shaderCubeFaceUv() ---
|
|
206
|
+
vec2 uv;
|
|
207
|
+
${faceUvGlsl}
|
|
208
|
+
|
|
209
|
+
// su/sv sign from resolved model: negative scale = mirror on axis.
|
|
210
|
+
// Bit offsets mirror WORD2.FLIP_U_SHIFT / WORD0.FLIP_V_SHIFT (GLSL cannot import TS).
|
|
211
|
+
if (((a_w2 >> 31u) & 0x1u) != 0u) uv.x = 1.0 - uv.x;
|
|
212
|
+
if (((a_w0 >> 31u) & 0x1u) != 0u) uv.y = 1.0 - uv.y;
|
|
213
|
+
v_uv = uv;
|
|
158
214
|
|
|
159
215
|
// --- Position: section base (multiples of 16) + face quad + block-local 0..15 ---
|
|
160
216
|
// Must mirror WORD2/WORD3 constants in TS (GLSL cannot import them).
|
|
@@ -321,6 +377,9 @@ void main() {
|
|
|
321
377
|
}
|
|
322
378
|
`
|
|
323
379
|
|
|
380
|
+
/** Vertex shader source (includes generated face UV table). Exported for parity tests. */
|
|
381
|
+
export const cubeBlockVertexShader = vertexShader
|
|
382
|
+
|
|
324
383
|
export function createCubeBlockMaterial(): THREE.ShaderMaterial {
|
|
325
384
|
return new THREE.ShaderMaterial({
|
|
326
385
|
vertexShader,
|
|
@@ -417,7 +476,8 @@ export const WORD0 = {
|
|
|
417
476
|
FACE_SHIFT: 12,
|
|
418
477
|
TINT_SHIFT: 15,
|
|
419
478
|
AO_SHIFT: 23,
|
|
420
|
-
|
|
479
|
+
/** sv<0 mirror flag — no free bits remain in word0 after this. */
|
|
480
|
+
FLIP_V_SHIFT: 31
|
|
421
481
|
} as const
|
|
422
482
|
|
|
423
483
|
export const WORD1 = {
|
|
@@ -434,7 +494,8 @@ export const WORD2 = {
|
|
|
434
494
|
SECTION_X_HI_SHIFT: 19,
|
|
435
495
|
SECTION_Z_HI_SHIFT: 25,
|
|
436
496
|
SECTION_HI_BITS: 6,
|
|
437
|
-
|
|
497
|
+
/** su<0 mirror flag — no free bits remain in word2 after this. */
|
|
498
|
+
FLIP_U_SHIFT: 31
|
|
438
499
|
} as const
|
|
439
500
|
|
|
440
501
|
/** Section base X/Z: low 16 bits in a_w3, high 6 in a_w2 (22-bit biased section index). */
|
|
@@ -5,6 +5,7 @@ import type { ExportedSection, ExportedWorldGeometry } from '../mesher-shared/ex
|
|
|
5
5
|
import { calculateSkyLightSimple } from '../lib/skyLight'
|
|
6
6
|
import { bakeLegacyVertexColors } from '../lib/bakeLegacyLight'
|
|
7
7
|
import { getShaderCubeResources } from '../wasm-mesher/bridge/shaderCubeBridge'
|
|
8
|
+
import { SHADER_CUBES_FORMAT_VERSION } from '../mesher-shared/shaderCubeFormat'
|
|
8
9
|
import { createCubeBlockMaterial, setCubeSkyLevel } from './shaders/cubeBlockShader'
|
|
9
10
|
import { createShaderCubeMesh } from './shaderCubeMesh'
|
|
10
11
|
|
|
@@ -181,6 +182,8 @@ export function createMeshesFromExport(
|
|
|
181
182
|
): THREE.Group[] {
|
|
182
183
|
const groups: THREE.Group[] = []
|
|
183
184
|
const resolvedShaderMat = shaderMaterial ?? shaderMaterialForExport(material, skyLevel)
|
|
185
|
+
let skippedShaderCubeSections = 0
|
|
186
|
+
let skippedShaderCubeFormatVersion: number | undefined
|
|
184
187
|
|
|
185
188
|
for (const section of exportData.sections) {
|
|
186
189
|
const group = new THREE.Group()
|
|
@@ -213,9 +216,14 @@ export function createMeshesFromExport(
|
|
|
213
216
|
|
|
214
217
|
const shaderCubes = section.shaderCubes
|
|
215
218
|
if (shaderCubes && shaderCubes.count > 0 && resolvedShaderMat) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
if (shaderCubes.formatVersion !== SHADER_CUBES_FORMAT_VERSION) {
|
|
220
|
+
skippedShaderCubeSections++
|
|
221
|
+
skippedShaderCubeFormatVersion ??= shaderCubes.formatVersion
|
|
222
|
+
} else {
|
|
223
|
+
const shaderMesh = createShaderCubeMesh(shaderCubes, resolvedShaderMat)
|
|
224
|
+
shaderMesh.position.set(section.position.x, section.position.y, section.position.z)
|
|
225
|
+
group.add(shaderMesh)
|
|
226
|
+
}
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
if (group.children.length > 0) {
|
|
@@ -223,6 +231,13 @@ export function createMeshesFromExport(
|
|
|
223
231
|
}
|
|
224
232
|
}
|
|
225
233
|
|
|
234
|
+
if (skippedShaderCubeSections > 0) {
|
|
235
|
+
console.warn(
|
|
236
|
+
`[worldGeometryExport] Skipped ${skippedShaderCubeSections} shader-cube section(s): ` +
|
|
237
|
+
`export format ${skippedShaderCubeFormatVersion} ≠ current ${SHADER_CUBES_FORMAT_VERSION}`
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
226
241
|
return groups
|
|
227
242
|
}
|
|
228
243
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
|
|
9
|
+
import { SHADER_CUBES_FORMAT_VERSION, SHADER_CUBES_WORDS_PER_FACE } from '../../mesher-shared/shaderCubeFormat'
|
|
9
10
|
import { WORD0, WORD1, WORD2, WORD3 } from '../../three/shaders/cubeBlockShader'
|
|
10
11
|
import { TextureIndexMapping, type TextureEntry } from '../../three/shaders/textureIndexMapping'
|
|
11
12
|
import { TintPalette } from '../../three/shaders/tintPalette'
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
export const SHADER_CUBES_WORDS_PER_FACE = 4 as const
|
|
14
|
+
export { SHADER_CUBES_FORMAT_VERSION, SHADER_CUBES_WORDS_PER_FACE } from '../../mesher-shared/shaderCubeFormat'
|
|
15
15
|
|
|
16
16
|
export type ShaderCubesOutput = {
|
|
17
17
|
words: Uint32Array
|
|
@@ -83,32 +83,78 @@ let tintPalette: TintPalette | null = null
|
|
|
83
83
|
let textureIndexMapping: TextureIndexMapping | null = null
|
|
84
84
|
let tintsMissingWarned = false
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
type FaceTextureRef = TextureEntry & { tileIndex?: number }
|
|
87
|
+
|
|
88
|
+
export type ResolvedFaceUv = { tileIndex: number; flipU: boolean; flipV: boolean }
|
|
89
|
+
|
|
90
|
+
const UV_RESOLVE_EPS = 1e-6
|
|
91
|
+
|
|
92
|
+
function isNearInteger(value: number): boolean {
|
|
93
|
+
return Math.abs(value - Math.round(value)) <= UV_RESOLVE_EPS
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a face to (atlas tile + mirror flags).
|
|
98
|
+
* null ⇒ face does not fit exactly one aligned tile; block must use legacy path.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveFaceUv(tex: FaceTextureRef, texMapping: TextureIndexMapping): ResolvedFaceUv | null {
|
|
101
|
+
const tileSize = 16
|
|
88
102
|
let u = tex.u ?? 0
|
|
89
103
|
let v = tex.v ?? 0
|
|
90
104
|
let su = tex.su ?? tileSize
|
|
91
105
|
let sv = tex.sv ?? tileSize
|
|
92
|
-
if (u > 0 && u <= 1) u = Math.round(u * atlasWidth)
|
|
93
|
-
if (v > 0 && v <= 1) v = Math.round(v * atlasWidth)
|
|
94
|
-
if (Math.abs(su) > 0 && Math.abs(su) <= 1) su = Math.round(Math.abs(su) * atlasWidth) || tileSize
|
|
95
|
-
else if (su < 0) su = tileSize
|
|
96
|
-
if (Math.abs(sv) > 0 && Math.abs(sv) <= 1) sv = Math.round(Math.abs(sv) * atlasWidth) || tileSize
|
|
97
|
-
else if (sv < 0) sv = tileSize
|
|
98
|
-
return { u, v, su, sv }
|
|
99
|
-
}
|
|
100
106
|
|
|
101
|
-
|
|
107
|
+
if (!Number.isFinite(u) || !Number.isFinite(v) || !Number.isFinite(su) || !Number.isFinite(sv)) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
const atlasWidth = texMapping.getTilesPerRow() * tileSize
|
|
112
|
+
if (Math.abs(su) <= 1) {
|
|
113
|
+
u *= atlasWidth
|
|
114
|
+
v *= atlasWidth
|
|
115
|
+
su *= atlasWidth
|
|
116
|
+
sv *= atlasWidth
|
|
109
117
|
}
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
|
|
119
|
+
if (!isNearInteger(u) || !isNearInteger(v) || !isNearInteger(su) || !isNearInteger(sv)) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const uPx = Math.round(u)
|
|
124
|
+
const vPx = Math.round(v)
|
|
125
|
+
const suPx = Math.round(su)
|
|
126
|
+
const svPx = Math.round(sv)
|
|
127
|
+
|
|
128
|
+
const flipU = suPx < 0
|
|
129
|
+
const flipV = svPx < 0
|
|
130
|
+
|
|
131
|
+
if (Math.abs(suPx) !== tileSize || Math.abs(svPx) !== tileSize) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const uMin = flipU ? uPx + suPx : uPx
|
|
136
|
+
const vMin = flipV ? vPx + svPx : vPx
|
|
137
|
+
|
|
138
|
+
if (uMin % tileSize !== 0 || vMin % tileSize !== 0) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
if (uMin < 0 || vMin < 0) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const derived = texMapping.tileIndexFromPixelCoords(uMin, vMin)
|
|
146
|
+
if (derived < 0) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof tex.tileIndex === 'number') {
|
|
151
|
+
if (tex.tileIndex !== derived) {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
return { tileIndex: derived, flipU, flipV }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { tileIndex: derived, flipU, flipV }
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
/** Main thread + worker: use `loadedData` set by the app / mesher (see mesherWasm). */
|
|
@@ -158,8 +204,8 @@ export function resetShaderCubeResources(): void {
|
|
|
158
204
|
|
|
159
205
|
/**
|
|
160
206
|
* Returns true when the block is a plain 1×1×1 cube that the instanced shader path
|
|
161
|
-
* can render
|
|
162
|
-
*
|
|
207
|
+
* can render with pixel-parity UV (no model rotation, single un-rotated element with
|
|
208
|
+
* all 6 cardinal faces present, each face exactly one aligned atlas tile).
|
|
163
209
|
* Pass the already-resolved model variant (`modelVars[variantIndex][0]`).
|
|
164
210
|
*/
|
|
165
211
|
export function isShaderCubeBlock(
|
|
@@ -191,7 +237,7 @@ export function isShaderCubeBlock(
|
|
|
191
237
|
if ((eFace as { rotation?: number }).rotation) return false
|
|
192
238
|
const tex = eFace.texture
|
|
193
239
|
if (!tex) return false
|
|
194
|
-
if (
|
|
240
|
+
if (resolveFaceUv(tex as FaceTextureRef, texMapping) === null) {
|
|
195
241
|
return false
|
|
196
242
|
}
|
|
197
243
|
}
|
|
@@ -199,7 +245,7 @@ export function isShaderCubeBlock(
|
|
|
199
245
|
return true
|
|
200
246
|
}
|
|
201
247
|
|
|
202
|
-
function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[]): number {
|
|
248
|
+
function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[], flipV: boolean): number {
|
|
203
249
|
let w = 0
|
|
204
250
|
w |= (lx & 0xf) << WORD0.LX_SHIFT
|
|
205
251
|
w |= (ly & 0xf) << WORD0.LY_SHIFT
|
|
@@ -209,6 +255,9 @@ function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex
|
|
|
209
255
|
for (let i = 0; i < WORD0.NUM_CORNERS; i++) {
|
|
210
256
|
w |= ((ao[i] ?? 3) & 3) << (WORD0.AO_SHIFT + i * WORD0.AO_BITS_PER_CORNER)
|
|
211
257
|
}
|
|
258
|
+
if (flipV) {
|
|
259
|
+
w |= 1 << WORD0.FLIP_V_SHIFT
|
|
260
|
+
}
|
|
212
261
|
return w >>> 0
|
|
213
262
|
}
|
|
214
263
|
|
|
@@ -224,7 +273,7 @@ function biasedSectionIndex(sectionBaseCoord: number): number {
|
|
|
224
273
|
return (Math.floor(sectionBaseCoord / 16) + WORD3.SECTION_BIAS) & WORD3.SECTION_MASK
|
|
225
274
|
}
|
|
226
275
|
|
|
227
|
-
export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number): number {
|
|
276
|
+
export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number, flipU = false): number {
|
|
228
277
|
let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
|
|
229
278
|
if (aoDiagonalFlip) {
|
|
230
279
|
w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
|
|
@@ -235,6 +284,9 @@ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBase
|
|
|
235
284
|
const sz = biasedSectionIndex(sectionBaseZ)
|
|
236
285
|
w |= ((sx >>> 16) & 0x3f) << WORD2.SECTION_X_HI_SHIFT
|
|
237
286
|
w |= ((sz >>> 16) & 0x3f) << WORD2.SECTION_Z_HI_SHIFT
|
|
287
|
+
if (flipU) {
|
|
288
|
+
w |= 1 << WORD2.FLIP_U_SHIFT
|
|
289
|
+
}
|
|
238
290
|
return w >>> 0
|
|
239
291
|
}
|
|
240
292
|
|
|
@@ -346,11 +398,12 @@ export function tryBuildShaderCubeInstances(
|
|
|
346
398
|
|
|
347
399
|
const eFace = faces[faceName]
|
|
348
400
|
const tex = eFace.texture! as FaceTextureRef
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
401
|
+
const resolved = resolveFaceUv(tex, textureIndexMapping)
|
|
402
|
+
if (resolved === null) {
|
|
351
403
|
words.length = wordsStart
|
|
352
404
|
return false
|
|
353
405
|
}
|
|
406
|
+
const { tileIndex: texIndex, flipU, flipV } = resolved
|
|
354
407
|
|
|
355
408
|
const rawAo = block.ao_data[faceDataIndex] ?? [3, 3, 3, 3]
|
|
356
409
|
const rawLight = lightCombinedForFace(block, faceDataIndex)
|
|
@@ -372,9 +425,9 @@ export function tryBuildShaderCubeInstances(
|
|
|
372
425
|
const tintIndex = tintPalette.getTintIndex(eFace.tintindex, cached.blockName, cached.blockProps, biome ?? 'plains')
|
|
373
426
|
|
|
374
427
|
words.push(
|
|
375
|
-
packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
|
|
428
|
+
packWord0(lx, ly, lz, faceIdx, tintIndex, ao, flipV),
|
|
376
429
|
packWord1(lightCombined),
|
|
377
|
-
packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z),
|
|
430
|
+
packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z, flipU),
|
|
378
431
|
packWord3(sectionOrigin.x, sectionOrigin.z)
|
|
379
432
|
)
|
|
380
433
|
}
|