@threepipe/plugin-blend-importer 0.1.0 → 0.3.1
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/CHANGELOG.md +53 -3
- package/README.md +25 -1
- package/dist/BlendLoadPlugin.d.ts +63 -3
- package/dist/BlendLoadPlugin.d.ts.map +1 -1
- package/dist/decompress.d.ts +10 -0
- package/dist/decompress.d.ts.map +1 -0
- package/dist/dependencies.txt +31 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1354 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1352 -87
- package/dist/index.mjs.map +1 -1
- package/dist/loader/camera.d.ts +1 -1
- package/dist/loader/ctx.d.ts +9 -1
- package/dist/loader/ctx.d.ts.map +1 -1
- package/dist/loader/geometry.d.ts +2 -2
- package/dist/loader/geometry.d.ts.map +1 -1
- package/dist/loader/index.d.ts +1 -1
- package/dist/loader/light.d.ts +1 -1
- package/dist/loader/material.d.ts +4 -1
- package/dist/loader/material.d.ts.map +1 -1
- package/dist/loader/mesh.d.ts +1 -1
- package/dist/loader/mesh.d.ts.map +1 -1
- package/docs/assets/highlight.css +49 -0
- package/docs/assets/icons.js +1 -1
- package/docs/assets/icons.svg +1 -1
- package/docs/assets/main.js +2 -2
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/assets/style.css +3 -3
- package/docs/classes/BlendLoadPlugin.html +6 -3
- package/docs/index.html +1 -1
- package/docs/interfaces/BlendFile.html +18 -0
- package/docs/interfaces/BlendLoadOptions.html +12 -0
- package/package.json +1 -1
- package/src/BlendLoadPlugin.ts +204 -13
- package/src/decompress.ts +59 -0
- package/src/index.ts +1 -0
- package/src/js-blend/main.js +4 -1
- package/src/js-blend/parser/parser.js +192 -52
- package/src/loader/ctx.ts +9 -0
- package/src/loader/geometry.ts +270 -13
- package/src/loader/material.ts +326 -8
- package/src/loader/mesh.ts +34 -8
- package/tsconfig.json +1 -0
- package/docs/.nojekyll +0 -1
package/src/loader/geometry.ts
CHANGED
|
@@ -12,10 +12,188 @@ function getLayer(layers: any, i: number) {
|
|
|
12
12
|
// https://projects.blender.org/blender/blender/issues/95967
|
|
13
13
|
// https://projects.blender.org/blender/blender/pulls/106638
|
|
14
14
|
|
|
15
|
+
// Read a NUL-terminated C string from a resolved pointer block (e.g. an Attribute.name char*).
|
|
16
|
+
function readCString(block: any): string | null {
|
|
17
|
+
if (!block || block.__data_address__ === undefined || !block.__blender_file__) return null
|
|
18
|
+
const u8 = block.__blender_file__.byte
|
|
19
|
+
let s = '', o = block.__data_address__
|
|
20
|
+
while (o < u8.length && u8[o] !== 0 && s.length < 256) s += String.fromCharCode(u8[o++])
|
|
21
|
+
return s
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Blender 5.0 (.blend file_format_version 1) stores mesh attributes in `attribute_storage`
|
|
25
|
+
// (struct AttributeStorage { Attribute *dna_attributes; int dna_attributes_num; ... }) instead of
|
|
26
|
+
// the CustomData vdata/ldata layers used by 2.8-4.x. See
|
|
27
|
+
// .repos/blender/source/blender/makesdna/DNA_attribute_types.h and BKE_attribute_enums.hh.
|
|
28
|
+
// Each Attribute { char *name; int16 data_type; int8 domain; int8 storage_type; void *data; } where
|
|
29
|
+
// data -> AttributeArray { void *data; ...; int64 size; } and the void* points to the packed values.
|
|
30
|
+
const ATTR_TYPE_INT32 = 3, ATTR_TYPE_FLOAT2 = 6, ATTR_TYPE_FLOAT3 = 7 // bke::AttrType
|
|
31
|
+
const ATTR_DOMAIN_CORNER = 3 // bke::AttrDomain (Point=0, Edge=1, Face=2, Corner=3)
|
|
32
|
+
function getAttributes(meshData: any): any[] {
|
|
33
|
+
const as = meshData.attribute_storage
|
|
34
|
+
if (!as) return []
|
|
35
|
+
let attrs = as.dna_attributes
|
|
36
|
+
if (!attrs) return []
|
|
37
|
+
attrs = (Array.isArray(attrs) || attrs.length !== undefined) ? Array.from(attrs) : [attrs]
|
|
38
|
+
return attrs as any[]
|
|
39
|
+
}
|
|
40
|
+
function findAttribute(attrs: any[], name: string, type?: number): any {
|
|
41
|
+
for (const a of attrs) {
|
|
42
|
+
if (readCString(a.name) === name && (type === undefined || a.data_type === type)) return a
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
// Element count actually stored for an attribute (AttributeArray.size). In Blender 5.0 the Mesh
|
|
47
|
+
// tot* fields are runtime (post-geometry-nodes) counts and DON'T match the stored array sizes — a
|
|
48
|
+
// geometry-nodes "Circle" can report totvert=15 while only 4 base vertices are stored. So the
|
|
49
|
+
// attribute's own size is authoritative; reading tot* elements overruns into adjacent blocks (garbage).
|
|
50
|
+
function attrSize(attr: any): number {
|
|
51
|
+
const arr = attr && attr.data
|
|
52
|
+
const n = arr && arr.size !== undefined ? Number(arr.size) : 0
|
|
53
|
+
return Number.isFinite(n) && n >= 0 ? n : 0
|
|
54
|
+
}
|
|
55
|
+
// Read the packed array for an Attribute as a TypedArray of `count` elements (alignment-safe).
|
|
56
|
+
function readAttrArray(attr: any, Ctor: any, count: number): any {
|
|
57
|
+
const arr = attr && attr.data // AttributeArray
|
|
58
|
+
const block = arr && arr.data // void* -> packed values block
|
|
59
|
+
if (!block || block.__data_address__ === undefined || !block.__blender_file__) return null
|
|
60
|
+
return block.__blender_file__.readTypedArray(Ctor, block.__data_address__, count)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Per-face material slots -> BufferGeometry groups ─────────────────
|
|
64
|
+
type GroupRun = {start: number, count: number, mat: number}
|
|
65
|
+
// Append a triangle run [start, start+count) (index-buffer offsets) tagged with material slot `mat`,
|
|
66
|
+
// merging into the previous run when contiguous and same-slot so the group count stays minimal.
|
|
67
|
+
function mergeGroupRun(runs: GroupRun[], start: number, count: number, mat: number) {
|
|
68
|
+
if (count <= 0) return
|
|
69
|
+
const last = runs[runs.length - 1]
|
|
70
|
+
if (last && last.mat === mat && last.start + last.count === start) last.count += count
|
|
71
|
+
else runs.push({start, count, mat})
|
|
72
|
+
}
|
|
73
|
+
// Mid path (corner_vert): per-face material slot is the `pdata` CustomData layer "material_index"
|
|
74
|
+
// (CD_PROP_INT32, type 11), one int per face. `pdata.layers` is a single object when totlayer===1.
|
|
75
|
+
function readFaceMaterialIndex(meshData: any, faceCount: number): number[] | null {
|
|
76
|
+
const pd = meshData.pdata
|
|
77
|
+
if (!pd || !pd.layers || !pd.totlayer) return null
|
|
78
|
+
for (let i = 0; i < pd.totlayer; i++) {
|
|
79
|
+
const l = getLayer(pd.layers, i)
|
|
80
|
+
if (l && l.name === 'material_index' && l.data) {
|
|
81
|
+
const data = Array.isArray(l.data) ? l.data : (l.data.length !== undefined ? Array.from(l.data) : null)
|
|
82
|
+
if (!data) return null
|
|
83
|
+
const out: number[] = new Array(faceCount)
|
|
84
|
+
for (let f = 0; f < faceCount; f++) {
|
|
85
|
+
const e: any = data[f]
|
|
86
|
+
out[f] = (e && typeof e === 'object') ? (e.i ?? e.value ?? 0) : (e || 0)
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build geometry from Blender 5.0 attribute_storage. Returns null if positions aren't available
|
|
95
|
+
// (so the caller can fall back to the vdata/ldata path).
|
|
96
|
+
function createBufferGeometryFromAttributes(meshData: any, ctx: Ctx): any {
|
|
97
|
+
const attrs = getAttributes(meshData)
|
|
98
|
+
if (!attrs.length) return null
|
|
99
|
+
|
|
100
|
+
const posAttr = findAttribute(attrs, 'position', ATTR_TYPE_FLOAT3)
|
|
101
|
+
const vertCount = attrSize(posAttr)
|
|
102
|
+
const posRaw = vertCount > 0 && readAttrArray(posAttr, Float32Array, vertCount * 3)
|
|
103
|
+
if (!posRaw) return null
|
|
104
|
+
|
|
105
|
+
const geometry = new ctx.BufferGeometry()
|
|
106
|
+
geometry.name = meshData.aname || ''
|
|
107
|
+
|
|
108
|
+
// Blender Z-up (x,y,z) -> three.js Y-up (x, z, -y). Sanitize non-finite components to 0 —
|
|
109
|
+
// real .blend files can contain NaN/Inf verts (e.g. from degenerate geometry-nodes ops), which
|
|
110
|
+
// Blender tolerates but three.js does not (computeBoundingBox/Sphere return NaN and break culling).
|
|
111
|
+
const positions = new Float32Array(vertCount * 3)
|
|
112
|
+
for (let j = 0; j < vertCount; j++) {
|
|
113
|
+
const x = posRaw[j * 3], y = posRaw[j * 3 + 1], z = posRaw[j * 3 + 2]
|
|
114
|
+
positions[j * 3] = Number.isFinite(x) ? x : 0
|
|
115
|
+
positions[j * 3 + 1] = Number.isFinite(z) ? z : 0
|
|
116
|
+
positions[j * 3 + 2] = Number.isFinite(y) ? -y : 0
|
|
117
|
+
}
|
|
118
|
+
geometry.setAttribute('position', new ctx.BufferAttribute(positions, 3))
|
|
119
|
+
|
|
120
|
+
// Faces: .corner_vert (vertex index per loop) + poly_offset_indices (loop range per face).
|
|
121
|
+
// Use the corner_vert array size for the loop count, and the poly_offset_indices DATA block's
|
|
122
|
+
// byte length for the face count (both are stored counts; Mesh.totloop/totpoly are runtime).
|
|
123
|
+
const cvAttr = findAttribute(attrs, '.corner_vert', ATTR_TYPE_INT32)
|
|
124
|
+
const loopCount = attrSize(cvAttr)
|
|
125
|
+
const cornerVerts = loopCount > 0 && readAttrArray(cvAttr, Int32Array, loopCount)
|
|
126
|
+
const poi = meshData.poly_offset_indices
|
|
127
|
+
const faceOffsetCount = poi && poi.__byte_length__ ? (poi.__byte_length__ >> 2) : 0
|
|
128
|
+
if (cornerVerts && faceOffsetCount > 1 && poi) {
|
|
129
|
+
const faceIndices = poi.__blender_file__.readTypedArray(Int32Array, poi.__data_address__, faceOffsetCount)
|
|
130
|
+
const faceCount = faceIndices.length - 1
|
|
131
|
+
let totalTriangles = 0
|
|
132
|
+
for (let i = 0; i < faceCount; i++) totalTriangles += Math.max(0, (faceIndices[i + 1] - faceIndices[i]) - 2)
|
|
133
|
+
const indexes = new Uint32Array(totalTriangles * 3)
|
|
134
|
+
const inRange = (v: number) => v >= 0 && v < vertCount
|
|
135
|
+
// Per-face material slot -> groups (only when >1 slot). material_index is a Face-domain Int32 attribute.
|
|
136
|
+
const matAttr = (meshData.totcol || 0) > 1 ? findAttribute(attrs, 'material_index', ATTR_TYPE_INT32) : null
|
|
137
|
+
const faceMat = matAttr && attrSize(matAttr) >= faceCount ? readAttrArray(matAttr, Int32Array, faceCount) : null
|
|
138
|
+
const groupRuns: GroupRun[] = []
|
|
139
|
+
let t = 0
|
|
140
|
+
for (let i = 0; i < faceCount; i++) {
|
|
141
|
+
const faceStart = faceIndices[i], faceVertCount = faceIndices[i + 1] - faceStart
|
|
142
|
+
const triStart = t
|
|
143
|
+
if (faceVertCount >= 3 && faceStart >= 0 && faceStart + faceVertCount <= loopCount) {
|
|
144
|
+
const firstVert = cornerVerts[faceStart] // todo: proper (ear-clip) triangulation for concave n-gons
|
|
145
|
+
for (let k = 1; k < faceVertCount - 1; k++) {
|
|
146
|
+
const b = cornerVerts[faceStart + k], c = cornerVerts[faceStart + k + 1]
|
|
147
|
+
// Skip triangles referencing out-of-range vertices (corrupt/partial geometry-nodes data).
|
|
148
|
+
if (inRange(firstVert) && inRange(b) && inRange(c)) {
|
|
149
|
+
indexes[t++] = firstVert
|
|
150
|
+
indexes[t++] = b
|
|
151
|
+
indexes[t++] = c
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (faceMat) mergeGroupRun(groupRuns, triStart, t - triStart, faceMat[i] || 0)
|
|
156
|
+
}
|
|
157
|
+
geometry.setIndex(new ctx.BufferAttribute(t === indexes.length ? indexes : indexes.slice(0, t), 1))
|
|
158
|
+
if (faceMat) for (const g of groupRuns) geometry.addGroup(g.start, g.count, g.mat)
|
|
159
|
+
|
|
160
|
+
// UVs: the first user UV map is a Float2 attribute on the Corner domain (named, not dot-prefixed;
|
|
161
|
+
// `.uv_select_*` are bools). UVs are per-corner but our geometry is per-vertex indexed, so we
|
|
162
|
+
// assign each vertex the UV of a corner referencing it (last write wins). This is exact away
|
|
163
|
+
// from UV seams; seam-correct UVs would need per-corner vertex expansion (todo).
|
|
164
|
+
// Blender and three.js share the same UV origin (bottom-left), so no V flip.
|
|
165
|
+
const uvAttr = attrs.find((a: any) => a.data_type === ATTR_TYPE_FLOAT2 && a.domain === ATTR_DOMAIN_CORNER && !(readCString(a.name) || '.').startsWith('.'))
|
|
166
|
+
const uvRaw = uvAttr && attrSize(uvAttr) >= loopCount && readAttrArray(uvAttr, Float32Array, loopCount * 2)
|
|
167
|
+
if (uvRaw) {
|
|
168
|
+
const uvs = new Float32Array(vertCount * 2)
|
|
169
|
+
for (let i = 0; i < loopCount; i++) {
|
|
170
|
+
const v = cornerVerts[i]
|
|
171
|
+
if (v >= 0 && v < vertCount) {
|
|
172
|
+
const u = uvRaw[i * 2], vv = uvRaw[i * 2 + 1]
|
|
173
|
+
uvs[v * 2] = Number.isFinite(u) ? u : 0
|
|
174
|
+
uvs[v * 2 + 1] = Number.isFinite(vv) ? vv : 0
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
geometry.setAttribute('uv', new ctx.BufferAttribute(uvs, 2))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (geometry.attributes.position && !geometry.attributes.normal)
|
|
182
|
+
geometry.computeVertexNormals()
|
|
183
|
+
return geometry
|
|
184
|
+
}
|
|
185
|
+
|
|
15
186
|
// https://github.com/blender/blender/blob/55e2fd2929b7577e0785c128c8f8069efd990c07/source/blender/blenkernel/intern/mesh.cc#L413
|
|
16
187
|
export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
17
188
|
|
|
18
189
|
if (meshData.mpoly) return createBufferGeometryOld(meshData, ctx)
|
|
190
|
+
|
|
191
|
+
// Blender 5.0+ mesh attribute storage (file_format_version 1). Falls through to the
|
|
192
|
+
// vdata/ldata path below if positions aren't found there.
|
|
193
|
+
if (meshData.attribute_storage) {
|
|
194
|
+
const g = createBufferGeometryFromAttributes(meshData, ctx)
|
|
195
|
+
if (g) return g
|
|
196
|
+
}
|
|
19
197
|
const geometry = new ctx.BufferGeometry()
|
|
20
198
|
geometry.name = meshData.aname || ''
|
|
21
199
|
|
|
@@ -26,7 +204,8 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
26
204
|
let vertices
|
|
27
205
|
let verticesData
|
|
28
206
|
let indices
|
|
29
|
-
let indicesData
|
|
207
|
+
let indicesData: any
|
|
208
|
+
let uvLayerData: any = null
|
|
30
209
|
|
|
31
210
|
// Extract vertex positions from vdata layers
|
|
32
211
|
if (meshData.vdata && meshData.vdata.layers && meshData.vdata.totlayer > 0) {
|
|
@@ -59,7 +238,15 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
59
238
|
const data = layer.data || []
|
|
60
239
|
// if (layer.name === '.corner_vert') { // type = 11
|
|
61
240
|
if (data.length === meshData.totloop) { // type = 11
|
|
62
|
-
|
|
241
|
+
const lname = layer.name || ''
|
|
242
|
+
// UV map: a Float2 corner layer (CD_PROP_FLOAT2, type 49), user-named (not dot-prefixed
|
|
243
|
+
// like `.corner_vert`/`.corner_edge`). Without this, textured meshes on the mid (3.6-4.x
|
|
244
|
+
// corner_vert) path render with no UVs and the base-colour texture cannot map.
|
|
245
|
+
if (!uvLayerData && (layer.type === 49 || (lname[0] !== '.' && /uv/i.test(lname)))) {
|
|
246
|
+
uvLayerData = data
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
if (indices && (lname !== '.corner_vert' || indices.name === '.corner_vert')) {
|
|
63
250
|
// console.warn('BlendLoader - multiple indices, ignoring', layer)
|
|
64
251
|
continue
|
|
65
252
|
}
|
|
@@ -103,14 +290,56 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
103
290
|
|
|
104
291
|
let faceIndices: number[] = []
|
|
105
292
|
if (meshData.poly_offset_indices && meshData.totpoly > 0) {
|
|
106
|
-
|
|
293
|
+
// Use the parser's alignment-safe reader — Blender packs blocks unpadded, so on v1
|
|
294
|
+
// (Blender 5.0) and some v0 files this offset isn't 4-aligned and a raw Int32Array view throws.
|
|
295
|
+
faceIndices = [...meshData.poly_offset_indices.__blender_file__.readTypedArray(Int32Array, meshData.poly_offset_indices.__data_address__, meshData.totpoly + 1)]
|
|
107
296
|
}
|
|
108
297
|
// if (faceSize !== 3 && faceSize !== 4 && !faceIndices.length) {
|
|
109
298
|
// console.error('not supported polygons with ', faceSize, 'vertices')
|
|
110
299
|
// return geometry
|
|
111
300
|
// }
|
|
112
301
|
|
|
113
|
-
|
|
302
|
+
// Per-corner vertex expansion for SEAM-CORRECT UVs (mid path). Blender stores UVs per corner (loop);
|
|
303
|
+
// three.js needs one UV per vertex. A Blender vertex shared by corners with DIFFERENT UVs is a UV seam —
|
|
304
|
+
// assigning it a single UV (last-write-wins) makes the faces across the seam interpolate the texture over
|
|
305
|
+
// the whole UV range, smearing/stretching it (classic symptom: the back of a UV-mapped sphere). Expand to
|
|
306
|
+
// one vertex per unique (blenderVert, uv) so seams split. Coincident split verts stay welded under the
|
|
307
|
+
// subsurf subdivision (same positions → same repositioning), so no crack. No UV layer ⇒ no expansion.
|
|
308
|
+
const vco = (vd: any): [number, number, number] => vd?.x !== undefined ? [vd.x, vd.y, vd.z] : (vd?.co ? [vd.co[0], vd.co[1], vd.co[2]] : [0, 0, 0])
|
|
309
|
+
let cornerToVert: Int32Array | null = null
|
|
310
|
+
let expandedPos: Float32Array | null = null
|
|
311
|
+
let expandedUv: Float32Array | null = null
|
|
312
|
+
if (uvLayerData && indicesData && verticesData && verticesData.length) {
|
|
313
|
+
cornerToVert = new Int32Array(indicesData.length)
|
|
314
|
+
const seen = new Map<string, number>()
|
|
315
|
+
const ps: number[] = [], us: number[] = []
|
|
316
|
+
const uvN = uvLayerData.length
|
|
317
|
+
for (let i = 0; i < indicesData.length; i++) {
|
|
318
|
+
const bv = indicesData[i]?.i
|
|
319
|
+
if (!(bv >= 0 && bv < verticesData.length)) { cornerToVert[i] = 0; continue }
|
|
320
|
+
const e = i < uvN ? uvLayerData[i] : null
|
|
321
|
+
const u = e && Number.isFinite(e.x) ? e.x : 0
|
|
322
|
+
const w = e && Number.isFinite(e.y) ? e.y : 0
|
|
323
|
+
const key = bv + '|' + Math.round(u * 4096) + '|' + Math.round(w * 4096)
|
|
324
|
+
let ev = seen.get(key)
|
|
325
|
+
if (ev === undefined) {
|
|
326
|
+
ev = us.length / 2
|
|
327
|
+
seen.set(key, ev)
|
|
328
|
+
const [x, y, z] = vco(verticesData[bv])
|
|
329
|
+
ps.push(x, z, -y) // Blender Z-up → three Y-up (same mapping as the non-expanded path)
|
|
330
|
+
us.push(u, w)
|
|
331
|
+
}
|
|
332
|
+
cornerToVert[i] = ev
|
|
333
|
+
}
|
|
334
|
+
expandedPos = new Float32Array(ps)
|
|
335
|
+
expandedUv = new Float32Array(us)
|
|
336
|
+
}
|
|
337
|
+
// Expanded-vertex index for a corner (loop) index; falls back to the raw Blender vertex when not expanding.
|
|
338
|
+
const vAt = (corner: number) => cornerToVert ? cornerToVert[corner] : indicesData[corner].i
|
|
339
|
+
|
|
340
|
+
if (expandedPos) {
|
|
341
|
+
geometry.setAttribute('position', new ctx.BufferAttribute(expandedPos, 3))
|
|
342
|
+
} else if (verticesData && verticesData.length > 0) {
|
|
114
343
|
const positions = new Float32Array(verticesData.length * 3)
|
|
115
344
|
for (let j = 0; j < verticesData.length; j++) {
|
|
116
345
|
const {x, y, z, co} = verticesData[j] || {}
|
|
@@ -137,8 +366,9 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
137
366
|
const faceSize = meshData.totloop / meshData.totpoly
|
|
138
367
|
if (faceIndices.length > 0) {
|
|
139
368
|
// Use face offset indices for variable-sized faces
|
|
369
|
+
const faceCount = faceIndices.length - 1
|
|
140
370
|
let totalTriangles = 0
|
|
141
|
-
for (let i = 0; i <
|
|
371
|
+
for (let i = 0; i < faceCount; i++) {
|
|
142
372
|
const faceVertCount = faceIndices[i + 1] - faceIndices[i]
|
|
143
373
|
totalTriangles += Math.max(0, faceVertCount - 2)
|
|
144
374
|
}
|
|
@@ -146,26 +376,33 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
146
376
|
const indexes = new Uint32Array(totalTriangles * 3)
|
|
147
377
|
let t = 0
|
|
148
378
|
|
|
149
|
-
|
|
379
|
+
// Per-face material slot -> groups (only when >1 slot). material_index lives in the `pdata` layer.
|
|
380
|
+
const faceMat = (meshData.totcol || 0) > 1 ? readFaceMaterialIndex(meshData, faceCount) : null
|
|
381
|
+
const groupRuns: GroupRun[] = []
|
|
382
|
+
|
|
383
|
+
for (let i = 0; i < faceCount; i++) {
|
|
150
384
|
const faceStart = faceIndices[i]
|
|
151
385
|
const faceEnd = faceIndices[i + 1]
|
|
152
386
|
const faceVertCount = faceEnd - faceStart
|
|
387
|
+
const triStart = t
|
|
153
388
|
|
|
154
389
|
if (faceVertCount >= 3) {
|
|
155
390
|
// todo better Triangulate the face using fan triangulation
|
|
156
|
-
const firstVert =
|
|
391
|
+
const firstVert = vAt(faceStart)
|
|
157
392
|
for (let k = 1; k < faceVertCount - 1; k++) {
|
|
158
393
|
indexes[t++] = firstVert
|
|
159
|
-
indexes[t++] =
|
|
160
|
-
indexes[t++] =
|
|
394
|
+
indexes[t++] = vAt(faceStart + k)
|
|
395
|
+
indexes[t++] = vAt(faceStart + k + 1)
|
|
161
396
|
}
|
|
162
397
|
} else {
|
|
163
398
|
// debugger
|
|
164
399
|
}
|
|
400
|
+
if (faceMat) mergeGroupRun(groupRuns, triStart, t - triStart, faceMat[i] || 0)
|
|
165
401
|
}
|
|
166
402
|
|
|
167
403
|
// console.log(indexes)
|
|
168
404
|
geometry.setIndex(new ctx.BufferAttribute(indexes, 1))
|
|
405
|
+
if (faceMat) for (const g of groupRuns) geometry.addGroup(g.start, g.count, g.mat)
|
|
169
406
|
} else if (faceSize === 3 || faceSize === 4) {
|
|
170
407
|
// Fall back to uniform face size approach
|
|
171
408
|
const isQuad = faceSize === 4
|
|
@@ -174,15 +411,15 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
174
411
|
|
|
175
412
|
if (faceSize !== 3 && faceSize !== 4) return geometry
|
|
176
413
|
for (let j = 0, t = 0; j < indices.length; j += faceSize) {
|
|
177
|
-
const a =
|
|
178
|
-
const b =
|
|
179
|
-
const c =
|
|
414
|
+
const a = vAt(j)
|
|
415
|
+
const b = vAt(j + 1)
|
|
416
|
+
const c = vAt(j + 2)
|
|
180
417
|
indexes[t++] = a
|
|
181
418
|
indexes[t++] = b
|
|
182
419
|
indexes[t++] = c
|
|
183
420
|
|
|
184
421
|
if (isQuad) {
|
|
185
|
-
const d =
|
|
422
|
+
const d = vAt(j + 3)
|
|
186
423
|
indexes[t++] = a
|
|
187
424
|
indexes[t++] = c
|
|
188
425
|
indexes[t++] = d
|
|
@@ -197,6 +434,12 @@ export function createBufferGeometry(meshData: any, ctx: Ctx) {
|
|
|
197
434
|
// console.log(geometry.attributes.position)
|
|
198
435
|
// console.log(geometry.index)
|
|
199
436
|
|
|
437
|
+
// UVs (mid corner_vert path): seam-correct, from the per-corner expansion above (one UV per split vertex).
|
|
438
|
+
// Blender and three.js share the same UV origin (bottom-left), so no V flip.
|
|
439
|
+
if (expandedUv && !geometry.attributes.uv) {
|
|
440
|
+
geometry.setAttribute('uv', new ctx.BufferAttribute(expandedUv, 2))
|
|
441
|
+
}
|
|
442
|
+
|
|
200
443
|
// compute stuff not present
|
|
201
444
|
if (geometry.attributes.position && !geometry.attributes.normal)
|
|
202
445
|
geometry.computeVertexNormals()
|
|
@@ -238,9 +481,16 @@ export function createBufferGeometryOld(mesh: any, ctx: Ctx) {
|
|
|
238
481
|
let currentIndex = 0
|
|
239
482
|
let computeNormals = false
|
|
240
483
|
|
|
484
|
+
// Per-face material slot (mat_nr) -> geometry groups, but only when the mesh has >1 slot. Faces are
|
|
485
|
+
// emitted in order, so each face's triangles form a contiguous index range; we merge consecutive
|
|
486
|
+
// same-slot faces into one group. mesh.ts then assigns one material per slot.
|
|
487
|
+
const useGroups = (mesh.totcol || 0) > 1
|
|
488
|
+
const groupRuns: GroupRun[] = []
|
|
489
|
+
|
|
241
490
|
for (const face of faces) {
|
|
242
491
|
const len = face.totloop
|
|
243
492
|
const start = face.loopstart
|
|
493
|
+
const faceStartIndex = currentIndex
|
|
244
494
|
let indexi = 1
|
|
245
495
|
|
|
246
496
|
while (indexi < len) {
|
|
@@ -283,6 +533,8 @@ export function createBufferGeometryOld(mesh: any, ctx: Ctx) {
|
|
|
283
533
|
|
|
284
534
|
indexi += 2
|
|
285
535
|
}
|
|
536
|
+
|
|
537
|
+
if (useGroups) mergeGroupRun(groupRuns, faceStartIndex, currentIndex - faceStartIndex, face.mat_nr || 0)
|
|
286
538
|
}
|
|
287
539
|
|
|
288
540
|
geometry.setAttribute('position', new ctx.BufferAttribute(positions, 3))
|
|
@@ -290,6 +542,11 @@ export function createBufferGeometryOld(mesh: any, ctx: Ctx) {
|
|
|
290
542
|
geometry.setAttribute('normal', new ctx.BufferAttribute(normals, 3))
|
|
291
543
|
geometry.setAttribute('uv', new ctx.BufferAttribute(uvs, 2))
|
|
292
544
|
|
|
545
|
+
// Emit groups for any multi-slot mesh: even a single run carries the correct slot index, so a mesh
|
|
546
|
+
// whose faces all use e.g. slot 2 renders with material[2] (not material[0]).
|
|
547
|
+
if (useGroups && groupRuns.length)
|
|
548
|
+
for (const g of groupRuns) geometry.addGroup(g.start, g.count, g.mat)
|
|
549
|
+
|
|
293
550
|
if (computeNormals) {
|
|
294
551
|
geometry.computeVertexNormals()
|
|
295
552
|
}
|