@threepipe/plugin-blend-importer 0.1.0 → 0.3.0
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 +45 -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 +1351 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1349 -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 +324 -8
- package/src/loader/mesh.ts +34 -8
- package/tsconfig.json +1 -0
- package/docs/.nojekyll +0 -1
package/src/loader/material.ts
CHANGED
|
@@ -1,14 +1,330 @@
|
|
|
1
|
-
|
|
2
|
-
// https://github.com/KhronosGroup/glTF-Blender-IO/blob/ed5100ab6c40472b7c3254fddfe0dd0d76d60644/addons/io_scene_gltf2/blender/exp/material/materials.py#L60
|
|
1
|
+
import {ClampToEdgeWrapping, DoubleSide, FrontSide, MirroredRepeatWrapping, RepeatWrapping, SRGBColorSpace, Texture} from 'threepipe'
|
|
3
2
|
import {Ctx} from './ctx'
|
|
4
3
|
|
|
4
|
+
// Material.blend_flag bit for the "Backface Culling" toggle (DNA_material_types.h: MA_BL_CULL_BACKFACE).
|
|
5
|
+
// The Python `use_backface_culling` property binds to this exact bit (rna_material.cc:1136).
|
|
6
|
+
const MA_BL_CULL_BACKFACE = 1 << 2
|
|
7
|
+
// Material.blend_method: alpha-clip cutout (DNA_material_types.h: MA_BM_CLIP). HASHED(4)/BLEND(5)/SOLID(0)
|
|
8
|
+
// map to alpha blend; only CLIP becomes an alpha test.
|
|
9
|
+
const MA_BM_CLIP = 3
|
|
10
|
+
|
|
11
|
+
// Faithful-ish port of Blender's glTF exporter material extraction (Principled BSDF -> glTF PBR),
|
|
12
|
+
// mapped onto three.js MeshPhysicalMaterial (which is glTF-first). Reference:
|
|
13
|
+
// .repos/gltf-blender-io/addons/io_scene_gltf2/blender/exp/material/{pbr_metallic_roughness,materials,search_node_tree}.py
|
|
14
|
+
// We read the shader that feeds the active Material Output, take each property from its named socket
|
|
15
|
+
// (constant default or a packed Image Texture), and apply the same defaults/clamps the exporter uses.
|
|
16
|
+
// Only packed images load (external file-path images are skipped). Linear vs sRGB matches glTF convention:
|
|
17
|
+
// base color + emissive factors are linear (Blender scene-linear == three.js linear); base/emissive
|
|
18
|
+
// textures are sRGB, all other maps (roughness/metallic/normal) are linear.
|
|
19
|
+
|
|
20
|
+
// ── Node-tree helpers ───────────────────────────────────────────────
|
|
21
|
+
function listBaseToArray(lb: any): any[] {
|
|
22
|
+
const out: any[] = []
|
|
23
|
+
if (!lb) return out
|
|
24
|
+
let n = lb.first, guard = 0
|
|
25
|
+
while (n && guard++ < 8192) { out.push(n); n = n.next }
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
28
|
+
const idnameOf = (n: any): string => (n && typeof n.idname === 'string' ? n.idname : '')
|
|
29
|
+
const nameOf = (s: any): string => (s && typeof s.name === 'string' ? s.name : '')
|
|
30
|
+
|
|
31
|
+
function socketByName(sockets: any[], ...names: string[]): any {
|
|
32
|
+
for (const name of names) {
|
|
33
|
+
const s = sockets.find(x => nameOf(x) === name)
|
|
34
|
+
if (s) return s
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
function inputsOf(node: any): any[] { return node ? listBaseToArray(node.inputs) : [] }
|
|
39
|
+
|
|
40
|
+
// A socket's constant value (bNodeSocketValueRGBA/Float/Vector expose default_value.value).
|
|
41
|
+
function socketConst(sock: any): any {
|
|
42
|
+
const dv = sock && sock.default_value
|
|
43
|
+
return dv ? dv.value : undefined
|
|
44
|
+
}
|
|
45
|
+
const num = (v: any): number | undefined => (typeof v === 'number' ? v : undefined)
|
|
46
|
+
const clamp01 = (c: number) => Math.max(0, Math.min(1, c))
|
|
47
|
+
|
|
48
|
+
// Follow the link into `socket`, hopping through Reroute nodes, and return the source node.
|
|
49
|
+
function nodeFeeding(links: any[], socket: any): any {
|
|
50
|
+
if (!socket) return null
|
|
51
|
+
let link = links.find(l => l.tosock === socket)
|
|
52
|
+
let guard = 0
|
|
53
|
+
while (link && idnameOf(link.fromnode).includes('Reroute') && guard++ < 32) {
|
|
54
|
+
const re = inputsOf(link.fromnode)[0]
|
|
55
|
+
link = links.find(l => l.tosock === re)
|
|
56
|
+
}
|
|
57
|
+
return link ? link.fromnode : null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Packed image -> three.js Texture ────────────────────────────────
|
|
61
|
+
function imageMime(b: Uint8Array): string | null {
|
|
62
|
+
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) return 'image/png'
|
|
63
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return 'image/jpeg'
|
|
64
|
+
if (b[0] === 0x42 && b[1] === 0x4d) return 'image/bmp'
|
|
65
|
+
if (b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 && b[8] === 0x57 && b[9] === 0x45) return 'image/webp'
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
// Load the packed image of an Image Texture node. `srgb` for color maps (base/emissive); false for data maps.
|
|
69
|
+
function packedTexture(imageNode: any, srgb: boolean): Texture | null {
|
|
70
|
+
const img = imageNode && imageNode.id
|
|
71
|
+
if (!img) return null
|
|
72
|
+
const pf = img.packedfile || (img.packedfiles && img.packedfiles.first && img.packedfiles.first.packedfile)
|
|
73
|
+
const block = pf && pf.data
|
|
74
|
+
const size = pf && typeof pf.size === 'number' ? pf.size : 0
|
|
75
|
+
if (!block || block.__data_address__ === undefined || size <= 0) return null
|
|
76
|
+
// Bounds-check before constructing the view: a stale/corrupt PackedFile.size or address would otherwise
|
|
77
|
+
// make `new Uint8Array(buffer, offset, size)` throw a RangeError that bubbles up and fails the whole
|
|
78
|
+
// import. Skip the one bad texture instead.
|
|
79
|
+
const buf = block.__blender_file__.byte.buffer
|
|
80
|
+
if (block.__data_address__ + size > buf.byteLength) return null
|
|
81
|
+
const bytes = new Uint8Array(buf, block.__data_address__, size).slice()
|
|
82
|
+
const mime = imageMime(bytes)
|
|
83
|
+
if (!mime || typeof Blob === 'undefined' || typeof URL === 'undefined' || typeof Image === 'undefined') return null
|
|
84
|
+
const url = URL.createObjectURL(new Blob([bytes], {type: mime}))
|
|
85
|
+
const el = new Image()
|
|
86
|
+
const texture = new Texture(el)
|
|
87
|
+
if (srgb) texture.colorSpace = SRGBColorSpace
|
|
88
|
+
texture.name = (typeof img.aname === 'string' ? img.aname : '') || 'blendTexture'
|
|
89
|
+
el.onload = () => { texture.needsUpdate = true; URL.revokeObjectURL(url) }
|
|
90
|
+
el.onerror = () => { URL.revokeObjectURL(url) }
|
|
91
|
+
el.src = url
|
|
92
|
+
return texture
|
|
93
|
+
}
|
|
94
|
+
// Image source enum (DNA_image_types.h): GENERATED = 4, VIEWER = 5 have no backing file.
|
|
95
|
+
const IMA_SRC_GENERATED = 4, IMA_SRC_VIEWER = 5
|
|
96
|
+
// Resolve an Image Texture node's image to a Texture: packed bytes if embedded, else an external file
|
|
97
|
+
// load through threepipe's pipeline (ctx.loadExternalTexture, when provided). External images reference a
|
|
98
|
+
// file by path in `Image.name` (e.g. "//tex/wood.png"). `source` is usually IMA_SRC_FILE (1), but some
|
|
99
|
+
// files store 0 for a file reference that wasn't loaded at save time — so treat any non-packed image with
|
|
100
|
+
// a path as external, excluding only GENERATED/VIEWER (which have no real file).
|
|
101
|
+
// Map the Image Texture node's "Extension" (NodeTexImage.extension) to a three.js wrap mode. Blender's
|
|
102
|
+
// DEFAULT is Repeat (0) — three.js defaults to ClampToEdge, so without this any mesh whose UVs fall outside
|
|
103
|
+
// [0,1] (very common: tiled materials, the Poly Haven preview sphere whose unwrap spans U[-0.5,1.5]) clamps
|
|
104
|
+
// to the edge texel and smears it into stripes instead of tiling. SHD_IMAGE_EXTENSION_*: 0 Repeat, 1 Extend,
|
|
105
|
+
// 2 Clip, 3 Mirror.
|
|
106
|
+
function applyWrap(tex: Texture | null, imageNode: any): Texture | null {
|
|
107
|
+
if (!tex) return tex
|
|
108
|
+
const ext = imageNode?.storage?.extension
|
|
109
|
+
tex.wrapS = tex.wrapT = ext === 1 || ext === 2 ? ClampToEdgeWrapping : ext === 3 ? MirroredRepeatWrapping : RepeatWrapping
|
|
110
|
+
tex.needsUpdate = true
|
|
111
|
+
return tex
|
|
112
|
+
}
|
|
113
|
+
function imageNodeTexture(imageNode: any, srgb: boolean, ctx: Ctx): Texture | null {
|
|
114
|
+
const packed = packedTexture(imageNode, srgb)
|
|
115
|
+
if (packed) return applyWrap(packed, imageNode)
|
|
116
|
+
const img = imageNode && imageNode.id
|
|
117
|
+
if (img && img.source !== IMA_SRC_GENERATED && img.source !== IMA_SRC_VIEWER
|
|
118
|
+
&& typeof img.name === 'string' && img.name && ctx.loadExternalTexture) {
|
|
119
|
+
return applyWrap(ctx.loadExternalTexture(img.name, srgb), imageNode)
|
|
120
|
+
}
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
// The Image Texture node feeding a socket (directly, or through a Normal Map node for normals), or null.
|
|
124
|
+
function imageNodeFeeding(links: any[], socket: any): any {
|
|
125
|
+
let src = nodeFeeding(links, socket)
|
|
126
|
+
if (src && idnameOf(src).includes('NormalMap')) src = nodeFeeding(links, socketByName(inputsOf(src), 'Color'))
|
|
127
|
+
return src && idnameOf(src).includes('TexImage') ? src : null
|
|
128
|
+
}
|
|
129
|
+
// Trace a socket to its Image Texture node and load it.
|
|
130
|
+
function textureFromSocket(links: any[], socket: any, srgb: boolean, ctx: Ctx): Texture | null {
|
|
131
|
+
const src = imageNodeFeeding(links, socket)
|
|
132
|
+
return src ? imageNodeTexture(src, srgb, ctx) : null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Surface shader resolution ───────────────────────────────────────
|
|
136
|
+
// Prefer the Principled BSDF feeding the active Material Output (matches the exporter's
|
|
137
|
+
// check_if_is_linked_to_active_output); fall back to any Principled in the tree.
|
|
138
|
+
function resolveShaders(nodes: any[], links: any[]): {principled: any, emission: any} {
|
|
139
|
+
const outputs = nodes.filter(n => idnameOf(n).includes('OutputMaterial'))
|
|
140
|
+
const activeOut = outputs.find(n => n.is_active_output) || outputs[0]
|
|
141
|
+
let principled: any = null, emission: any = null
|
|
142
|
+
if (activeOut) {
|
|
143
|
+
let surf = nodeFeeding(links, socketByName(inputsOf(activeOut), 'Surface'))
|
|
144
|
+
// hop through a single Mix/Add Shader to find a Principled, best-effort
|
|
145
|
+
let guard = 0
|
|
146
|
+
while (surf && !idnameOf(surf).includes('BsdfPrincipled') && guard++ < 4) {
|
|
147
|
+
if (idnameOf(surf).includes('Emission') && !emission) emission = surf
|
|
148
|
+
const next = inputsOf(surf).map(s => nodeFeeding(links, s)).find(n => n && idnameOf(n).includes('BsdfPrincipled'))
|
|
149
|
+
if (!next) break
|
|
150
|
+
surf = next
|
|
151
|
+
}
|
|
152
|
+
if (surf && idnameOf(surf).includes('BsdfPrincipled')) principled = surf
|
|
153
|
+
}
|
|
154
|
+
if (!principled) principled = nodes.find(n => idnameOf(n).includes('BsdfPrincipled'))
|
|
155
|
+
if (!emission) emission = nodes.find(n => idnameOf(n) === 'ShaderNodeEmission')
|
|
156
|
+
return {principled, emission}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build a PhysicalMaterial from a Blender material, following Blender's glTF exporter mapping.
|
|
161
|
+
*/
|
|
5
162
|
export function createMaterial(mat: any, ctx: Ctx) {
|
|
6
163
|
const material = new ctx.MeshPhysicalMaterial()
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
material.
|
|
164
|
+
|
|
165
|
+
// Blender materials default to backface culling OFF, i.e. they render double-sided. The glTF exporter
|
|
166
|
+
// maps this directly: doubleSided = not use_backface_culling (materials.py:289). three.js defaults to
|
|
167
|
+
// FrontSide, so without this most thin/open geometry (planes, leaves, interiors) is invisible from
|
|
168
|
+
// behind. Only the explicit "Backface Culling" toggle (MA_BL_CULL_BACKFACE) forces single-sided.
|
|
169
|
+
material.side = (typeof mat.blend_flag === 'number' && (mat.blend_flag & MA_BL_CULL_BACKFACE)) ? FrontSide : DoubleSide
|
|
170
|
+
|
|
171
|
+
const nodes = mat.nodetree ? listBaseToArray(mat.nodetree.nodes) : []
|
|
172
|
+
const links = mat.nodetree ? listBaseToArray(mat.nodetree.links) : []
|
|
173
|
+
const {principled, emission} = resolveShaders(nodes, links)
|
|
174
|
+
|
|
175
|
+
let applied = false
|
|
176
|
+
if (principled) {
|
|
177
|
+
const inp = inputsOf(principled)
|
|
178
|
+
applied = true
|
|
179
|
+
|
|
180
|
+
// Base color: texture takes precedence; factor (clamped) otherwise. (pbr_metallic_roughness.py:73)
|
|
181
|
+
const baseSock = socketByName(inp, 'Base Color', 'BaseColor')
|
|
182
|
+
const baseImgNode = imageNodeFeeding(links, baseSock)
|
|
183
|
+
const baseTex = baseImgNode ? imageNodeTexture(baseImgNode, true, ctx) : null
|
|
184
|
+
if (baseTex) {
|
|
185
|
+
material.map = baseTex
|
|
186
|
+
material.color.setRGB(1, 1, 1)
|
|
187
|
+
} else {
|
|
188
|
+
const base = socketConst(baseSock)
|
|
189
|
+
if (base && base.length >= 3) material.color.setRGB(clamp01(base[0]), clamp01(base[1]), clamp01(base[2]))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Roughness / Metallic (factor or grayscale map; three.js reads G/B but greyscale maps work).
|
|
193
|
+
const roughSock = socketByName(inp, 'Roughness')
|
|
194
|
+
const roughTex = textureFromSocket(links, roughSock, false, ctx)
|
|
195
|
+
if (roughTex) material.roughnessMap = roughTex
|
|
196
|
+
else { const r = num(socketConst(roughSock)); if (r !== undefined) material.roughness = r }
|
|
197
|
+
const metalSock = socketByName(inp, 'Metallic')
|
|
198
|
+
const metalTex = textureFromSocket(links, metalSock, false, ctx)
|
|
199
|
+
if (metalTex) material.metalnessMap = metalTex
|
|
200
|
+
else { const m = num(socketConst(metalSock)); if (m !== undefined) material.metalness = m }
|
|
201
|
+
|
|
202
|
+
// Normal map + strength (materials.py:376; Normal Map node "Strength").
|
|
203
|
+
const normSrc = nodeFeeding(links, socketByName(inp, 'Normal'))
|
|
204
|
+
if (normSrc) {
|
|
205
|
+
const normTex = textureFromSocket(links, socketByName(inp, 'Normal'), false, ctx)
|
|
206
|
+
if (normTex) {
|
|
207
|
+
material.normalMap = normTex
|
|
208
|
+
const strength = num(socketConst(socketByName(inputsOf(normSrc), 'Strength')))
|
|
209
|
+
if (strength !== undefined && material.normalScale) material.normalScale.set(strength, strength)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Alpha -> opacity (constant) or alphaMap (texture), plus the alpha *mode* from the material's
|
|
214
|
+
// Eevee blend_method. Only applied when the material actually has an alpha input, so opaque
|
|
215
|
+
// materials are never made see-through (notably the default material, which reports HASHED but
|
|
216
|
+
// is fully opaque at alpha=1). Mode mapping is the canonical glTF one: CLIP -> alpha test
|
|
217
|
+
// (cutout, using alpha_threshold); HASHED/BLEND/SOLID -> alpha blend. The 4.2+ glTF exporter
|
|
218
|
+
// derives this from the node graph, but blend_method is the authoritative stored value for <=4.1
|
|
219
|
+
// and a usable hint after (gather_alpha_info — search_node_tree.py).
|
|
220
|
+
const alphaSock = socketByName(inp, 'Alpha')
|
|
221
|
+
const alphaImgNode = imageNodeFeeding(links, alphaSock)
|
|
222
|
+
// If Alpha comes from the SAME image as Base Color (the common cutout setup: Image.Color -> Base
|
|
223
|
+
// Color, Image.Alpha -> Alpha), three.js material.map (RGBA) already supplies per-pixel opacity from
|
|
224
|
+
// the texture's alpha channel. A separate material.alphaMap would instead read the GREEN channel
|
|
225
|
+
// (three.js samples alphaMap.g), giving wrong cutouts — so only use a distinct alphaMap when the
|
|
226
|
+
// alpha image differs from the base image.
|
|
227
|
+
const alphaFromBase = !!(alphaImgNode && baseImgNode && alphaImgNode === baseImgNode)
|
|
228
|
+
const alphaTex = (alphaImgNode && !alphaFromBase) ? imageNodeTexture(alphaImgNode, false, ctx) : null
|
|
229
|
+
const alphaC = num(socketConst(alphaSock))
|
|
230
|
+
const hasAlpha = !!alphaTex || alphaFromBase || (alphaC !== undefined && alphaC < 1.0)
|
|
231
|
+
if (alphaTex) material.alphaMap = alphaTex
|
|
232
|
+
else if (!alphaFromBase && alphaC !== undefined && alphaC < 1.0) material.opacity = alphaC
|
|
233
|
+
if (hasAlpha) {
|
|
234
|
+
if (mat.blend_method === MA_BM_CLIP) {
|
|
235
|
+
const thr = num(mat.alpha_threshold)
|
|
236
|
+
material.alphaTest = thr !== undefined && thr > 0 ? thr : 0.5
|
|
237
|
+
} else material.transparent = true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// IOR (extensions/ior.py — default 1.5).
|
|
241
|
+
const ior = num(socketConst(socketByName(inp, 'IOR')))
|
|
242
|
+
if (ior !== undefined && (material as any).ior !== undefined) (material as any).ior = ior
|
|
243
|
+
|
|
244
|
+
// Transmission (extensions/transmission.py — "Transmission Weight"/"Transmission").
|
|
245
|
+
const trans = num(socketConst(socketByName(inp, 'Transmission Weight', 'Transmission')))
|
|
246
|
+
if (trans !== undefined && trans > 0 && (material as any).transmission !== undefined) {
|
|
247
|
+
(material as any).transmission = trans
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Clearcoat / Coat (extensions/clearcoat.py — "Coat Weight"/"Clearcoat", roughness default 0.03).
|
|
251
|
+
const coat = num(socketConst(socketByName(inp, 'Coat Weight', 'Clearcoat')))
|
|
252
|
+
if (coat !== undefined && coat > 0 && (material as any).clearcoat !== undefined) {
|
|
253
|
+
(material as any).clearcoat = coat
|
|
254
|
+
const cr = num(socketConst(socketByName(inp, 'Coat Roughness', 'Clearcoat Roughness')))
|
|
255
|
+
if (cr !== undefined) (material as any).clearcoatRoughness = cr
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Sheen (extensions/sheen.py — "Sheen Weight"/"Sheen" gates; color + roughness).
|
|
259
|
+
const sheen = num(socketConst(socketByName(inp, 'Sheen Weight', 'Sheen')))
|
|
260
|
+
if (sheen !== undefined && sheen > 0 && (material as any).sheen !== undefined) {
|
|
261
|
+
(material as any).sheen = sheen
|
|
262
|
+
const stint = socketConst(socketByName(inp, 'Sheen Tint'))
|
|
263
|
+
if (stint && stint.length >= 3 && (material as any).sheenColor) (material as any).sheenColor.setRGB(stint[0], stint[1], stint[2])
|
|
264
|
+
const sr = num(socketConst(socketByName(inp, 'Sheen Roughness')))
|
|
265
|
+
if (sr !== undefined) (material as any).sheenRoughness = sr
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Specular IOR Level -> specularIntensity (extensions/specular.py — factor is ×2, three.js range 0..1).
|
|
269
|
+
const spec = num(socketConst(socketByName(inp, 'Specular IOR Level', 'Specular')))
|
|
270
|
+
if (spec !== undefined && (material as any).specularIntensity !== undefined) {
|
|
271
|
+
(material as any).specularIntensity = Math.min(1, spec * 2)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Displacement: Material Output "Displacement" <- Displacement node <- Height image. three.js
|
|
276
|
+
// `displacementMap` moves vertices along the normal (needs tessellated geometry); we also drive
|
|
277
|
+
// `bumpMap` from the same height so the relief is visible on coarse meshes, but only when there is no
|
|
278
|
+
// normal map (bump + normal would double-perturb). Scale comes from the Displacement node, clamped.
|
|
279
|
+
const matOutput = nodes.find(n => idnameOf(n).includes('OutputMaterial') && n.is_active_output)
|
|
280
|
+
|| nodes.find(n => idnameOf(n).includes('OutputMaterial'))
|
|
281
|
+
const dispNode = matOutput ? nodeFeeding(links, socketByName(inputsOf(matOutput), 'Displacement')) : null
|
|
282
|
+
if (dispNode && idnameOf(dispNode).includes('Displacement')) {
|
|
283
|
+
const heightTex = textureFromSocket(links, socketByName(inputsOf(dispNode), 'Height'), false, ctx)
|
|
284
|
+
if (heightTex) {
|
|
285
|
+
const scale = num(socketConst(socketByName(inputsOf(dispNode), 'Scale')))
|
|
286
|
+
const s = scale !== undefined && scale > 0 ? scale : 1
|
|
287
|
+
const mid = num(socketConst(socketByName(inputsOf(dispNode), 'Midlevel')))
|
|
288
|
+
if ((material as any).displacementMap !== undefined) {
|
|
289
|
+
(material as any).displacementMap = heightTex
|
|
290
|
+
;(material as any).displacementScale = s
|
|
291
|
+
;(material as any).displacementBias = -(mid ?? 0.5) * s
|
|
292
|
+
}
|
|
293
|
+
// Bump fallback for un-tessellated meshes — only when the material has no normal map.
|
|
294
|
+
if (!material.normalMap && (material as any).bumpMap !== undefined) {
|
|
295
|
+
(material as any).bumpMap = heightTex
|
|
296
|
+
;(material as any).bumpScale = s
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Emission: dedicated Emission node "Color"/"Strength", else Principled "Emission Color"/"Emission".
|
|
302
|
+
const emSock = emission ? socketByName(inputsOf(emission), 'Color')
|
|
303
|
+
: (principled ? socketByName(inputsOf(principled), 'Emission Color', 'Emission') : null)
|
|
304
|
+
const emStrSock = emission ? socketByName(inputsOf(emission), 'Strength')
|
|
305
|
+
: (principled ? socketByName(inputsOf(principled), 'Emission Strength') : null)
|
|
306
|
+
const emTex = textureFromSocket(links, emSock, true, ctx)
|
|
307
|
+
let emCol = socketConst(emSock)
|
|
308
|
+
// When a texture drives emission, the Emission Color factor multiplies it. Blender's default factor is
|
|
309
|
+
// black [0,0,0] (and stays black even with a texture linked), which would zero out the map — so treat a
|
|
310
|
+
// missing/short/all-black factor as white so the emissive map is actually visible.
|
|
311
|
+
if (emTex && (!emCol || emCol.length < 3 || (!emCol[0] && !emCol[1] && !emCol[2]))) emCol = [1, 1, 1]
|
|
312
|
+
if (emCol && emCol.length >= 3 && (emCol[0] || emCol[1] || emCol[2] || emTex)) {
|
|
313
|
+
const f = [emCol[0], emCol[1], emCol[2]]
|
|
314
|
+
const strength = num(socketConst(emStrSock))
|
|
315
|
+
if (typeof strength === 'number') for (let i = 0; i < 3; i++) f[i] *= strength
|
|
316
|
+
// Split intensity from color like the exporter (materials.py:149): keep color <=1, push the rest into intensity.
|
|
317
|
+
const peak = Math.max(f[0], f[1], f[2])
|
|
318
|
+
if (peak > 1) { material.emissiveIntensity = peak; material.emissive.setRGB(f[0] / peak, f[1] / peak, f[2] / peak) }
|
|
319
|
+
else material.emissive.setRGB(f[0], f[1], f[2])
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!applied) {
|
|
323
|
+
// Legacy fallback — Blender material's viewport diffuse color (often just 0.8 grey).
|
|
324
|
+
if (mat.r !== undefined) material.color.setRGB(mat.r, mat.g, mat.b)
|
|
325
|
+
material.roughness = mat.roughness !== undefined ? mat.roughness : 0.4
|
|
326
|
+
material.metalness = mat.metallic !== undefined ? mat.metallic : 0.0
|
|
327
|
+
}
|
|
328
|
+
|
|
13
329
|
return material
|
|
14
330
|
}
|
package/src/loader/mesh.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {DoubleSide} from 'threepipe'
|
|
1
2
|
import {createBufferGeometry} from './geometry'
|
|
2
3
|
import {createMaterial} from './material'
|
|
3
4
|
import {Ctx} from './ctx'
|
|
@@ -7,17 +8,42 @@ export function createMesh(object: any, loaded: WeakMap<any, any>, ctx: Ctx) {
|
|
|
7
8
|
return undefined
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
let geometry = loaded.get(object.data)
|
|
12
|
+
if (!geometry) {
|
|
13
|
+
geometry = createBufferGeometry(object.data, ctx)
|
|
14
|
+
// Modifiers (subdivision surface, mirror, array, solidify) are evaluated on the `blend-importer-modifiers` branch, not here yet.
|
|
15
|
+
loaded.set(object.data, geometry)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Material slots: `object.data.mat` is the slot array (length `totcol`); faces reference slots by
|
|
19
|
+
// index. When a mesh uses more than one slot, the geometry builder tags triangles with their slot
|
|
20
|
+
// via `geometry.groups`; we then hand three.js the full material array (indexed by group.materialIndex).
|
|
21
|
+
// Otherwise a single material is used (the common case).
|
|
22
|
+
const matData = object.data.mat
|
|
23
|
+
const slots: any[] = Array.isArray(matData) ? matData : (matData ? [matData] : [])
|
|
12
24
|
|
|
13
|
-
const
|
|
25
|
+
const buildMaterial = (m: any) => {
|
|
26
|
+
if (!m) { const d = new ctx.MeshPhysicalMaterial(); d.side = DoubleSide; return d } // empty slot -> Blender default
|
|
27
|
+
const cached = loaded.get(m)
|
|
28
|
+
if (cached) return cached
|
|
29
|
+
const created = createMaterial(m, ctx)
|
|
30
|
+
loaded.set(m, created)
|
|
31
|
+
return created
|
|
32
|
+
}
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
34
|
+
const hasGroups = !!(geometry.groups && geometry.groups.length)
|
|
35
|
+
let material: any
|
|
36
|
+
if (hasGroups) {
|
|
37
|
+
material = slots.map(buildMaterial) // multi-material: array indexed by geometry.groups' materialIndex
|
|
38
|
+
// Defensive: a stale/over-range material_index (corrupt data) would map to an undefined entry and
|
|
39
|
+
// three.js would silently skip those faces (invisible). Pad with a default so they stay visible.
|
|
40
|
+
let maxIdx = 0
|
|
41
|
+
for (const g of geometry.groups) if (g.materialIndex > maxIdx) maxIdx = g.materialIndex
|
|
42
|
+
while (material.length <= maxIdx) material.push(buildMaterial(null))
|
|
43
|
+
} else {
|
|
44
|
+
material = buildMaterial(slots[0])
|
|
45
|
+
}
|
|
18
46
|
|
|
19
|
-
// console.log(material, mat)
|
|
20
|
-
// material.side = DoubleSide
|
|
21
47
|
const mesh = new ctx.Mesh(geometry, material)
|
|
22
48
|
|
|
23
49
|
mesh.castShadow = true
|
package/tsconfig.json
CHANGED
package/docs/.nojekyll
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|