@solidrt/3d 0.0.63 → 0.0.65
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/AGENTS.md +338 -48
- package/examples/README.md +3 -3
- package/examples/fog.tsx +18 -5
- package/examples/model-load.tsx +1 -1
- package/package.json +7 -3
- package/src/camera.ts +6 -2
- package/src/components/directional-light.tsx +6 -2
- package/src/components/hemisphere-light.tsx +12 -4
- package/src/components/index.ts +2 -0
- package/src/components/instanced-mesh.tsx +6 -2
- package/src/components/mesh.tsx +12 -1
- package/src/components/point-light.tsx +6 -2
- package/src/components/record-mesh.tsx +14 -2
- package/src/components/scene.tsx +8 -1
- package/src/components/splat-mesh.tsx +50 -0
- package/src/components/spot-light.tsx +7 -3
- package/src/environment.ts +55 -0
- package/src/geometry.ts +1 -1
- package/src/glsl.ts +69 -15
- package/src/gltf.ts +98 -15
- package/src/index.ts +12 -7
- package/src/light.ts +56 -4
- package/src/material.ts +26 -1
- package/src/mesh.ts +361 -6
- package/src/model-data.ts +9 -7
- package/src/model-file.ts +16 -7
- package/src/model.ts +40 -20
- package/src/node.ts +8 -0
- package/src/scene-pointer.ts +6 -5
- package/src/scene-shadows.ts +8 -7
- package/src/scene.ts +225 -104
- package/src/splat-data.ts +617 -0
- package/src/splat.ts +401 -0
- package/tools/model.flux.ts +206 -0
- package/tools/splat.ts +70 -0
- package/tools/model.ts +0 -56
package/src/glsl.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import type { CullMode, VertexAttribute } from "@solidrt/core/gpu"
|
|
20
20
|
|
|
21
21
|
// core/gpu's glsl tag, aliased locally (it is String.raw) so this module
|
|
22
|
-
// stays runtime-pure: no flux:gpu import, so the
|
|
22
|
+
// stays runtime-pure: no flux:gpu import, so the tests and any
|
|
23
23
|
// bake-time tool can run it headless on bare flux.
|
|
24
24
|
let glsl = String.raw
|
|
25
25
|
|
|
@@ -368,9 +368,12 @@ export const PBR = glsl`
|
|
|
368
368
|
* unit vector TOWARD a directional light; a spot's axis toward the
|
|
369
369
|
* light; unused for a point), `uLightPos[i]` (a spot's or point's world
|
|
370
370
|
* position; unused for a directional), `uLightColor[i]` (rgb, intensity
|
|
371
|
-
* folded in)
|
|
371
|
+
* folded in), `uLightParams[i]` (cosInner, cosOuter, distance, decay
|
|
372
372
|
* - the cone cosines a spot fades between, the falloff cutoff and
|
|
373
|
-
* exponent a spot or point attenuates by; unused for a directional)
|
|
373
|
+
* exponent a spot or point attenuates by; unused for a directional) and
|
|
374
|
+
* `uLightMask[i]` (the light's `layers` filter bitmask, all bits by
|
|
375
|
+
* default - lightVector skips a light whose mask misses the mesh's
|
|
376
|
+
* `uLayers`, see LIGHT_LOOKUP).
|
|
374
377
|
* Compose it before LIGHT_LOOKUP; the stock lit program is the shape.
|
|
375
378
|
*/
|
|
376
379
|
export const LIGHT_SLOTS = glsl`
|
|
@@ -380,6 +383,7 @@ export const LIGHT_SLOTS = glsl`
|
|
|
380
383
|
uniform vec3 uLightPos[${MAX_LIGHTS}];
|
|
381
384
|
uniform vec3 uLightColor[${MAX_LIGHTS}];
|
|
382
385
|
uniform vec4 uLightParams[${MAX_LIGHTS}];
|
|
386
|
+
uniform int uLightMask[${MAX_LIGHTS}];
|
|
383
387
|
const int LIGHT_DIRECTIONAL = 0;
|
|
384
388
|
const int LIGHT_SPOT = 1;
|
|
385
389
|
const int LIGHT_POINT = 2;
|
|
@@ -395,13 +399,24 @@ export const LIGHT_SLOTS = glsl`
|
|
|
395
399
|
* spot's additionally faded across its cone from cosInner to cosOuter.
|
|
396
400
|
* Zero means the light cannot reach the fragment - skip its shadow
|
|
397
401
|
* lookup and its terms (the stock lit program does exactly that).
|
|
402
|
+
* The first test is the LAYER gate: a light whose `uLightMask[i]`
|
|
403
|
+
* misses the mesh's `uLayers` (the per-entry membership bitmask the
|
|
404
|
+
* scene seeds from `mesh.layers` and follows through setLayers) cannot
|
|
405
|
+
* reach it at all - Unity's cullingMask, Godot's light_cull_mask. Both
|
|
406
|
+
* operands are uniforms, so the branch is coherent per draw.
|
|
398
407
|
*/
|
|
399
408
|
export const LIGHT_LOOKUP = glsl`
|
|
400
409
|
// Floors the falloff divisors so a fragment at the light's own
|
|
401
410
|
// position stays finite (Three's punctual-light rule).
|
|
402
411
|
const float FALLOFF_MIN = 0.01;
|
|
403
412
|
|
|
413
|
+
uniform int uLayers;
|
|
414
|
+
|
|
404
415
|
float lightVector(int i, vec3 worldPos, out vec3 l) {
|
|
416
|
+
if ((uLightMask[i] & uLayers) == 0) {
|
|
417
|
+
l = vec3(0.0);
|
|
418
|
+
return 0.0;
|
|
419
|
+
}
|
|
405
420
|
if (uLightType[i] == LIGHT_DIRECTIONAL) {
|
|
406
421
|
l = uLightDir[i];
|
|
407
422
|
return 1.0;
|
|
@@ -523,6 +538,36 @@ export const SRGB = glsl`
|
|
|
523
538
|
}
|
|
524
539
|
`
|
|
525
540
|
|
|
541
|
+
/**
|
|
542
|
+
* The buffer's blend space (SceneOptions.blendSpace): `uBlendSpace` is 0
|
|
543
|
+
* in a linear-light scene (the default; every scene writes it, since a
|
|
544
|
+
* shared program's uniform keeps the last drawn value across targets)
|
|
545
|
+
* and 1 in a display-space one, where the buffer holds sRGB-encoded
|
|
546
|
+
* display pixels and BLENDING runs on encoded values - what a capture
|
|
547
|
+
* trained against sRGB compositing expects. `vec3 blendSpaceOutput(vec3
|
|
548
|
+
* rgb, float alpha)` is the branch a buffer-writing fragment ends with:
|
|
549
|
+
* premultiplied linear in, and out either the same value (linear scene)
|
|
550
|
+
* or the un-premultiplied color clamped, encoded and re-premultiplied
|
|
551
|
+
* (display scene). Every stock fragment composes it through sceneOutput
|
|
552
|
+
* (and the skybox on its own); a CUSTOM fragment written for a
|
|
553
|
+
* display-space scene must end with it too, or its output blends in the
|
|
554
|
+
* wrong space. The encode carries its own name (not SRGB's
|
|
555
|
+
* linearToSrgb) so composing this set never collides with a fragment
|
|
556
|
+
* that also composes SRGB.
|
|
557
|
+
*/
|
|
558
|
+
export const BLEND_SPACE = glsl`
|
|
559
|
+
uniform float uBlendSpace;
|
|
560
|
+
vec3 blendSpaceEncode(vec3 c) {
|
|
561
|
+
c = clamp(c, 0.0, 1.0);
|
|
562
|
+
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
|
|
563
|
+
}
|
|
564
|
+
vec3 blendSpaceOutput(vec3 rgb, float alpha) {
|
|
565
|
+
if (uBlendSpace < 0.5) return rgb;
|
|
566
|
+
vec3 c = alpha > 0.0 ? rgb / alpha : rgb;
|
|
567
|
+
return blendSpaceEncode(c) * alpha;
|
|
568
|
+
}
|
|
569
|
+
`
|
|
570
|
+
|
|
526
571
|
/**
|
|
527
572
|
* The LOD cross-fade: `uLodFade` is (threshold, side) - the scene writes
|
|
528
573
|
* the band position of the level this entry draws (see createLod's
|
|
@@ -1079,7 +1124,8 @@ export type SceneSourceOptions = {
|
|
|
1079
1124
|
* vec4 sceneOutput(vec3 rgb, float alpha, vec3 position);
|
|
1080
1125
|
*
|
|
1081
1126
|
* `sceneLight` is light i seen from a world point: `dir` the unit vector
|
|
1082
|
-
* TOWARD it, `color` its rgb already attenuated (falloff, cone)
|
|
1127
|
+
* TOWARD it, `color` its rgb already attenuated (falloff, cone), layer
|
|
1128
|
+
* masked (a light whose `layers` misses the mesh's, see LIGHT_LOOKUP) and
|
|
1083
1129
|
* shadowed - zero when it cannot reach the fragment, and then no shadow
|
|
1084
1130
|
* tap was spent. It is the accessor a custom light model loops over to
|
|
1085
1131
|
* `uLightCount` (URP's GetAdditionalLight, Godot's light() inputs), and
|
|
@@ -1095,12 +1141,17 @@ export type SceneSourceOptions = {
|
|
|
1095
1141
|
* stage carries the world position in.
|
|
1096
1142
|
*
|
|
1097
1143
|
* `sceneOutput` is the tail every library fragment ends with: fog over
|
|
1098
|
-
* the premultiplied color, clamped at zero,
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1144
|
+
* the premultiplied color, clamped at zero, then the blend-space branch
|
|
1145
|
+
* (BLEND_SPACE), and that is the pixel the scene buffer holds. In a
|
|
1146
|
+
* linear scene (the default) that is LINEAR light, unclamped above (a
|
|
1147
|
+
* sun disc of 40 stays 40 in a half-float buffer), and exposure, tone
|
|
1148
|
+
* mapping and the sRGB encode happen once per pixel in the target's
|
|
1149
|
+
* resolve pass (RESOLVE), never in a scene fragment. In a display-space
|
|
1150
|
+
* scene (SceneOptions.blendSpace) the tail encodes here instead, the
|
|
1151
|
+
* buffer holds display pixels and there is no resolve. A fragment that
|
|
1152
|
+
* writes fragColor directly follows the same contract: linear
|
|
1153
|
+
* premultiplied light in a linear scene, blendSpaceOutput's result in a
|
|
1154
|
+
* display-space one.
|
|
1104
1155
|
*
|
|
1105
1156
|
* ${SCENE}
|
|
1106
1157
|
* void main() {
|
|
@@ -1125,6 +1176,7 @@ export function sceneSource(o: SceneSourceOptions = {}): string {
|
|
|
1125
1176
|
lights
|
|
1126
1177
|
? `uniform vec3 uHemiSky;
|
|
1127
1178
|
uniform vec3 uHemiGround;
|
|
1179
|
+
uniform int uHemiMask;
|
|
1128
1180
|
${LIGHT_SLOTS}
|
|
1129
1181
|
${LIGHT_LOOKUP}`
|
|
1130
1182
|
: ""
|
|
@@ -1147,10 +1199,11 @@ export function sceneSource(o: SceneSourceOptions = {}): string {
|
|
|
1147
1199
|
${env ? ENVIRONMENT : ""}
|
|
1148
1200
|
${fog ? FOG : ""}
|
|
1149
1201
|
${lights ? sceneShadeSource(receiveShadow, env) : ""}
|
|
1202
|
+
${BLEND_SPACE}
|
|
1150
1203
|
|
|
1151
1204
|
vec4 sceneOutput(vec3 rgb, float alpha, vec3 position) {
|
|
1152
1205
|
${fog === "additive" ? "rgb = fogAdditive(rgb, position, uCamPos);" : fog ? "rgb = fog(rgb, alpha, position, uCamPos);" : ""}
|
|
1153
|
-
return vec4(max(rgb, vec3(0.0)), alpha);
|
|
1206
|
+
return vec4(blendSpaceOutput(max(rgb, vec3(0.0)), alpha), alpha);
|
|
1154
1207
|
}
|
|
1155
1208
|
`
|
|
1156
1209
|
}
|
|
@@ -1160,8 +1213,9 @@ export const SCENE = sceneSource()
|
|
|
1160
1213
|
|
|
1161
1214
|
// The light accessor and the two shade functions, over the sets
|
|
1162
1215
|
// sceneSource composed before them. A light that cannot reach the point
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1216
|
+
// - out of falloff, outside its cone, or layer-masked off the mesh - is
|
|
1217
|
+
// skipped whole (no shadow tap, no terms), the gate the stock lit
|
|
1218
|
+
// program always had. The hemisphere masks the same way (uHemiMask).
|
|
1165
1219
|
function sceneShadeSource(receiveShadow: boolean, env: boolean): string {
|
|
1166
1220
|
return glsl`
|
|
1167
1221
|
struct SceneLight {
|
|
@@ -1180,7 +1234,7 @@ function sceneShadeSource(receiveShadow: boolean, env: boolean): string {
|
|
|
1180
1234
|
vec3 shadeBlinn(Surface s, vec3 position) {
|
|
1181
1235
|
vec3 n = s.normal;
|
|
1182
1236
|
vec3 v = normalize(uCamPos - position);
|
|
1183
|
-
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
1237
|
+
vec3 light = (uHemiMask & uLayers) != 0 ? hemisphere(n, uHemiSky, uHemiGround) : vec3(0.0);
|
|
1184
1238
|
light += s.ambient;
|
|
1185
1239
|
vec3 spec = vec3(0.0);
|
|
1186
1240
|
for (int i = 0; i < ${MAX_LIGHTS}; i++) {
|
|
@@ -1201,7 +1255,7 @@ function sceneShadeSource(receiveShadow: boolean, env: boolean): string {
|
|
|
1201
1255
|
vec3 v = normalize(uCamPos - position);
|
|
1202
1256
|
vec3 diffuseColor = s.base.rgb * (1.0 - s.metalness);
|
|
1203
1257
|
vec3 f0 = mix(vec3(DIELECTRIC_F0) * s.base.a, s.base.rgb, s.metalness);
|
|
1204
|
-
vec3 light = hemisphere(n, uHemiSky, uHemiGround);
|
|
1258
|
+
vec3 light = (uHemiMask & uLayers) != 0 ? hemisphere(n, uHemiSky, uHemiGround) : vec3(0.0);
|
|
1205
1259
|
${env ? "light += envIrradiance(n);" : ""}
|
|
1206
1260
|
light += s.ambient;
|
|
1207
1261
|
vec3 spec = vec3(0.0);
|
package/src/gltf.ts
CHANGED
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
// data: buffers and images, and single-file .glb.
|
|
17
17
|
//
|
|
18
18
|
// Pure module by design - a parse is JSON plus typed-array views plus one
|
|
19
|
-
// interleave loop per primitive, so it runs the same under bun (
|
|
20
|
-
//
|
|
19
|
+
// interleave loop per primitive, so it runs the same under bun (an app's
|
|
20
|
+
// bake script, the check rig) and on flux (loadGltf in
|
|
21
21
|
// model.ts). It never decodes images: material.map indexes the encoded
|
|
22
|
-
//
|
|
22
|
+
// files in `images`, and uploading is the engine side's job. An image is
|
|
23
|
+
// a PNG or JPEG, or a KTX2 compressed texture (KHR_texture_basisu, whose
|
|
24
|
+
// source a texture prefers over its plain one), kept with what its
|
|
25
|
+
// source calls it.
|
|
23
26
|
//
|
|
24
27
|
// Outside the subset: Draco/meshopt-compressed meshes and any other
|
|
25
28
|
// required extension throw naming it; tangents (the base channel and
|
|
@@ -44,6 +47,8 @@
|
|
|
44
47
|
// the way Three fills userData, so app data authored as custom
|
|
45
48
|
// properties arrives with the model.
|
|
46
49
|
|
|
50
|
+
import type { TextureKind } from "@solidrt/core/textures"
|
|
51
|
+
|
|
47
52
|
import { compose, decompose, det3, mat4, multiply, quatNormalize } from "./math.ts"
|
|
48
53
|
import { linearToSrgb } from "./color.ts"
|
|
49
54
|
import type { Mat4, Quat, Vec3 } from "./math.ts"
|
|
@@ -229,8 +234,13 @@ export type ModelData = {
|
|
|
229
234
|
/** The file's animations (empty when it has none). */
|
|
230
235
|
clips: ModelClip[]
|
|
231
236
|
materials: ModelMaterial[]
|
|
232
|
-
/**
|
|
233
|
-
images:
|
|
237
|
+
/** The images the materials' maps index. */
|
|
238
|
+
images: ModelImage[]
|
|
239
|
+
/** The KTX2 images transcoded for this device, by image index (the
|
|
240
|
+
* others absent): what `transcodeModelImages` adds and `createModel`
|
|
241
|
+
* uploads in place of the file. Never in a file; a parse and
|
|
242
|
+
* `decodeModel` leave it out. */
|
|
243
|
+
textures?: (ModelTexture | undefined)[]
|
|
234
244
|
/** World-space rest-pose [minX, minY, minZ, maxX, maxY, maxZ] over every
|
|
235
245
|
* part - each part's local box through its node's composed transform, and
|
|
236
246
|
* a skinned part's per-joint boxes through the joints' rest transforms
|
|
@@ -355,6 +365,68 @@ const DEFAULT_MATERIAL: ModelMaterial = {
|
|
|
355
365
|
}
|
|
356
366
|
|
|
357
367
|
/** True when the bytes are a .glb container (the "glTF" magic). */
|
|
368
|
+
/** One image of a model: its file as it is, and what its source calls
|
|
369
|
+
* it. */
|
|
370
|
+
export type ModelImage = {
|
|
371
|
+
/** The encoded image file: PNG, JPEG or KTX2 bytes. */
|
|
372
|
+
bytes: Uint8Array
|
|
373
|
+
/** Its name in the document; without one the uri of its file, and
|
|
374
|
+
* "image<i>" (its index in the document) for a nameless image embedded
|
|
375
|
+
* there. Labels its texture, and is how an image embedded in a model
|
|
376
|
+
* is told from the others (`solidrt.textures`). */
|
|
377
|
+
name: string
|
|
378
|
+
/** The file a parse read it from, as the document names it (still
|
|
379
|
+
* percent-encoded), when it is a file of its own. Absent for an image
|
|
380
|
+
* embedded in its document, and on everything read from the container,
|
|
381
|
+
* which embeds its images. */
|
|
382
|
+
uri?: string
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** How a model samples one of its images: what decides its color space
|
|
386
|
+
* at upload and how it is compressed at bake. */
|
|
387
|
+
export type ModelImageUse = {
|
|
388
|
+
/** A material samples it as color (base color, emissive): its texels
|
|
389
|
+
* are sRGB-encoded, as glTF stores those. Every other map is linear. */
|
|
390
|
+
srgb: boolean
|
|
391
|
+
/** What its errors cost, which picks its codec at bake time (the kinds
|
|
392
|
+
* of `solidrt.textures`): "normal" when any material samples it as a
|
|
393
|
+
* normal map, else "color" when it is a color map, else "data". An
|
|
394
|
+
* image several slots share takes the most demanding of them, and
|
|
395
|
+
* stays sRGB if one of them is a color slot. */
|
|
396
|
+
kind: TextureKind
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* How the model's materials sample each of its images, by image index.
|
|
401
|
+
* The one rule behind createModel's uploads and the bake's encodes, so a
|
|
402
|
+
* baked image is sampled as the same image unbaked.
|
|
403
|
+
*/
|
|
404
|
+
export function modelImageUses(data: Pick<ModelData, "images" | "materials">): ModelImageUse[] {
|
|
405
|
+
let uses: ModelImageUse[] = data.images.map(() => ({ srgb: false, kind: "data" }))
|
|
406
|
+
let at = (index: number | null): ModelImageUse | undefined => (index === null ? undefined : uses[index])
|
|
407
|
+
for (let m of data.materials) {
|
|
408
|
+
for (let color of [at(m.map), at(m.emissiveMap)]) {
|
|
409
|
+
if (color === undefined) continue
|
|
410
|
+
color.srgb = true
|
|
411
|
+
if (color.kind === "data") color.kind = "color"
|
|
412
|
+
}
|
|
413
|
+
let normal = at(m.normalMap)
|
|
414
|
+
if (normal !== undefined) normal.kind = "normal"
|
|
415
|
+
}
|
|
416
|
+
return uses
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** A compressed texture ready to upload: the payload `createTexture`
|
|
420
|
+
* takes at `format`, with the full mip chain when `mipmap`. The shape
|
|
421
|
+
* `transcodeTexture` returns. */
|
|
422
|
+
export type ModelTexture = {
|
|
423
|
+
data: Uint8Array
|
|
424
|
+
width: number
|
|
425
|
+
height: number
|
|
426
|
+
format: "etc2-rgba8" | "etc2-rgba8-srgb" | "bc7-rgba8" | "bc7-rgba8-srgb" | "rgba8" | "rgba8-srgb"
|
|
427
|
+
mipmap: boolean
|
|
428
|
+
}
|
|
429
|
+
|
|
358
430
|
export function isGlb(bytes: Uint8Array): boolean {
|
|
359
431
|
return bytes.length >= 12 && new DataView(bytes.buffer, bytes.byteOffset, 12).getUint32(0, true) === GLB_MAGIC
|
|
360
432
|
}
|
|
@@ -363,7 +435,7 @@ export function isGlb(bytes: Uint8Array): boolean {
|
|
|
363
435
|
// samples, and so the only images the parser ever opens: the prefetch
|
|
364
436
|
// list (gltfExternalUris) and the parser's demand set are both derived
|
|
365
437
|
// from this one table, so a channel added to the map is added here too or
|
|
366
|
-
//
|
|
438
|
+
// tests/gltf.test.ts fails on its all-channels fixture.
|
|
367
439
|
const SAMPLED_TEXTURES: ((material: any) => any)[] = [
|
|
368
440
|
(m) => m.pbrMetallicRoughness?.baseColorTexture,
|
|
369
441
|
(m) => m.normalTexture,
|
|
@@ -371,14 +443,21 @@ const SAMPLED_TEXTURES: ((material: any) => any)[] = [
|
|
|
371
443
|
(m) => m.pbrMetallicRoughness?.metallicRoughnessTexture,
|
|
372
444
|
]
|
|
373
445
|
|
|
446
|
+
// The image a texture samples: the KTX2 one where the file carries it
|
|
447
|
+
// (KHR_texture_basisu), ahead of the plain source a file may keep beside
|
|
448
|
+
// it as a fallback for loaders without the extension.
|
|
449
|
+
function textureSource(texture: any): number | undefined {
|
|
450
|
+
return texture?.extensions?.KHR_texture_basisu?.source ?? texture?.source
|
|
451
|
+
}
|
|
452
|
+
|
|
374
453
|
// The image indices a document's materials sample (SAMPLED_TEXTURES
|
|
375
|
-
// through
|
|
454
|
+
// through textureSource), the demand set imageSlot will meet.
|
|
376
455
|
function sampledImages(gltf: any): Set<number> {
|
|
377
456
|
let images = new Set<number>()
|
|
378
457
|
for (let m of gltf.materials ?? []) {
|
|
379
458
|
for (let channel of SAMPLED_TEXTURES) {
|
|
380
459
|
let ref = channel(m)
|
|
381
|
-
let source = ref === undefined ? undefined : gltf.textures?.[ref.index]
|
|
460
|
+
let source = ref === undefined ? undefined : textureSource(gltf.textures?.[ref.index])
|
|
382
461
|
if (source !== undefined) images.add(source)
|
|
383
462
|
}
|
|
384
463
|
}
|
|
@@ -429,9 +508,9 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
429
508
|
}
|
|
430
509
|
// Quantized attributes read through the normalized-integer path,
|
|
431
510
|
// emissive strength is the emissive intensity, GPU instancing is
|
|
432
|
-
// read as placements
|
|
433
|
-
// the file means.
|
|
434
|
-
if (ext !== "KHR_mesh_quantization" && ext !== "KHR_materials_emissive_strength" && ext !== "EXT_mesh_gpu_instancing") {
|
|
511
|
+
// read as placements, a KTX2 image is an image like another; every
|
|
512
|
+
// other required extension changes what the file means.
|
|
513
|
+
if (ext !== "KHR_mesh_quantization" && ext !== "KHR_materials_emissive_strength" && ext !== "EXT_mesh_gpu_instancing" && ext !== "KHR_texture_basisu") {
|
|
435
514
|
throw new Error("parseGltf: the file requires the " + ext + " extension, which is not supported")
|
|
436
515
|
}
|
|
437
516
|
}
|
|
@@ -462,7 +541,7 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
462
541
|
|
|
463
542
|
// Images are pulled in only when a material samples them, in first-use
|
|
464
543
|
// order, so `map` indexes a compact list.
|
|
465
|
-
let images:
|
|
544
|
+
let images: ModelImage[] = []
|
|
466
545
|
let imageSlots = new Map<number, number>()
|
|
467
546
|
let imageSlot = (index: number): number => {
|
|
468
547
|
let slot = imageSlots.get(index)
|
|
@@ -470,8 +549,12 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
470
549
|
let image = gltf.images?.[index]
|
|
471
550
|
if (image === undefined) throw new Error("parseGltf: texture names a missing image " + index)
|
|
472
551
|
let bytes = image.uri !== undefined ? external(image.uri, "image " + index) : bufferViewBytes(image.bufferView)
|
|
552
|
+
// A data: uri is the image embedded in the document, not a file.
|
|
553
|
+
let file: string | undefined = image.uri !== undefined && !image.uri.startsWith("data:") ? image.uri : undefined
|
|
554
|
+
let entry: ModelImage = { bytes, name: image.name ?? file ?? "image" + index }
|
|
555
|
+
if (file !== undefined) entry.uri = file
|
|
473
556
|
slot = images.length
|
|
474
|
-
images.push(
|
|
557
|
+
images.push(entry)
|
|
475
558
|
imageSlots.set(index, slot)
|
|
476
559
|
}
|
|
477
560
|
return slot
|
|
@@ -484,8 +567,8 @@ export function parseGltf(bytes: Uint8Array, resolve?: UriResolver): ModelData {
|
|
|
484
567
|
// (the prefetch list): a new channel goes in the table first.
|
|
485
568
|
let textureSlot = (ref: any): number | null => {
|
|
486
569
|
if (ref === undefined) return null
|
|
487
|
-
let
|
|
488
|
-
return
|
|
570
|
+
let source = textureSource(gltf.textures?.[ref.index])
|
|
571
|
+
return source !== undefined ? imageSlot(source) : null
|
|
489
572
|
}
|
|
490
573
|
let materials: ModelMaterial[] = (gltf.materials ?? []).map((m: any, i: number): ModelMaterial => {
|
|
491
574
|
let pbr = m.pbrMetallicRoughness ?? {}
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
export { add, createGroup, destroy, getMorphNames, getMorphWeights, getRotation, getTransform, lookAt, remove, setMorphWeights, setTransform, setTransition, setVisible, worldPosition } from "./node.ts"
|
|
9
9
|
export type { LodConfig, MorphWeights, NodePointerEvent, NodeTapEvent, NodeWheelEvent, SceneEventBase, SceneNode, ScenePointerEvent, ScenePointerListener, SceneTapEvent, SceneWheelEvent, TransformUpdate, TransitionEndEvent } from "./node.ts"
|
|
10
|
-
export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, setCastShadow, setCulling, setDrawRange, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, instanceAttribute, updateRecords, INSTANCE_FLOATS } from "./mesh.ts"
|
|
11
|
-
export type { InstancedMesh as InstancedMeshNode, InstancedMeshOptions, InstanceNode, InstanceSlots, InstanceStream, Mesh as MeshNode, MeshInstances, RecordMesh as RecordMeshNode, RecordMeshOptions, UpdateRecordsOptions } from "./mesh.ts"
|
|
10
|
+
export { addInstance, createInstancedMesh, createMesh, createRecordMesh, createSprite, disposeInstances, setCastShadow, setCulling, setDrawRange, setEnvironment, setGeometry, setInstanceStyle, setLayers, setMaterial, setMeshParams, setRecordCount, setRecords, setRenderOrder, instanceAttribute, updateRecords, INSTANCE_FLOATS } from "./mesh.ts"
|
|
11
|
+
export type { InstancedMesh as InstancedMeshNode, InstancedMeshOptions, InstanceNode, InstanceOrderOptions, InstanceSlots, InstanceStream, Mesh as MeshNode, MeshInstances, RecordMesh as RecordMeshNode, RecordMeshOptions, UpdateRecordsOptions } from "./mesh.ts"
|
|
12
12
|
export { createDirectionalLight, createHemisphereLight, createPointLight, createSpotLight, setLight } from "./light.ts"
|
|
13
13
|
// The shader-source caps, also on the /glsl subpath next to the sources they size.
|
|
14
14
|
export { MAX_CASCADES, MAX_LIGHTS, MAX_SHADOW_MAPS } from "./glsl.ts"
|
|
@@ -31,7 +31,7 @@ export { createInstancedLod, createLod, setLod } from "./lod.ts"
|
|
|
31
31
|
export type { InstancedLodLevel, InstancedLodOptions, LodLevel, LodOptions } from "./lod.ts"
|
|
32
32
|
export { createScene, resolveParams } from "./scene.ts"
|
|
33
33
|
export { feedPointer } from "./scene-pointer.ts"
|
|
34
|
-
export type { BloomOptions, Capsule, EnvironmentOptions, FogOptions, Hit, Impact, MoveOptions, MoveResult, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, ResolveInput, ResolveOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, ViewHandle, ViewOptions, Volume } from "./scene.ts"
|
|
34
|
+
export type { BlendSpace, BloomOptions, Capsule, EnvironmentOptions, FogOptions, Hit, Impact, MoveOptions, MoveResult, OrientedBox, Overlap, QueryOptions, ReflectionProbe, ReflectionProbeOptions, ResolveInput, ResolveOptions, Scene as SceneHandle, SceneHandlers, SceneOptions, ScreenRay, SkyboxOptions, Sphere, ToneMapping, ViewHandle, ViewOptions, Volume } from "./scene.ts"
|
|
35
35
|
export { moveAndSlide } from "./collision.ts"
|
|
36
36
|
export type { MoveScene } from "./collision.ts"
|
|
37
37
|
export { bufferFormat, equirectToCube, loadEnvironment } from "./environment.ts"
|
|
@@ -49,7 +49,7 @@ export { extrude, lathe, pathFrames, polygon, sweep, tube } from "./sweep.ts"
|
|
|
49
49
|
export type { ExtrudeOptions, LatheOptions, PathFrames, PathPoint, SweepPath, TubeOptions } from "./sweep.ts"
|
|
50
50
|
export { phong, shaderMaterial, shaderMaterialClass, sprite, standard, unlit } from "./material.ts"
|
|
51
51
|
export type { LitOptions, Material, PhongOptions, ShaderMaterialClass, ShaderMaterialClassOptions, ShaderMaterialInstanceOptions, ShaderMaterialOptions, SpriteOptions, StandardOptions, UnlitOptions } from "./material.ts"
|
|
52
|
-
export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedLod, InstancedMesh, Lod, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SpotLight, Sprite, View3d, useScene, Shot, Shots } from "./components/index.ts"
|
|
52
|
+
export { DirectionalLight, FirstPersonCamera, Group, HemisphereLight, Instance, InstancedLod, InstancedMesh, Lod, Mesh, OrbitCamera, PerspectiveCamera, PointLight, RecordMesh, Scene, SplatMesh, SpotLight, Sprite, View3d, useScene, Shot, Shots } from "./components/index.ts"
|
|
53
53
|
export type {
|
|
54
54
|
CameraTarget,
|
|
55
55
|
DirectionalLightProps,
|
|
@@ -69,6 +69,7 @@ export type {
|
|
|
69
69
|
RecordMeshProps,
|
|
70
70
|
ScenePointerProps,
|
|
71
71
|
SceneProps,
|
|
72
|
+
SplatMeshProps,
|
|
72
73
|
SpotLightProps,
|
|
73
74
|
SpriteProps,
|
|
74
75
|
TransformProps,
|
|
@@ -76,11 +77,15 @@ export type {
|
|
|
76
77
|
ShotProps,
|
|
77
78
|
ShotsProps,
|
|
78
79
|
} from "./components/index.ts"
|
|
79
|
-
export { gltfExternalUris, isGlb, parseGltf } from "./gltf.ts"
|
|
80
|
-
export type { ModelChannel, ModelClip, ModelData, ModelExtras, ModelMaterial, ModelNode, ModelPart, ModelSkin, UriResolver } from "./gltf.ts"
|
|
80
|
+
export { gltfExternalUris, isGlb, modelImageUses, parseGltf } from "./gltf.ts"
|
|
81
|
+
export type { ModelChannel, ModelClip, ModelData, ModelExtras, ModelImage, ModelImageUse, ModelMaterial, ModelNode, ModelPart, ModelSkin, ModelTexture, UriResolver } from "./gltf.ts"
|
|
81
82
|
export { decodeModel, encodeModel } from "./model-file.ts"
|
|
82
|
-
export { createModel, loadGltf, loadModel } from "./model.ts"
|
|
83
|
+
export { createModel, loadGltf, loadModel, transcodeModelImages } from "./model.ts"
|
|
83
84
|
export type { Model, ModelMaps, ModelOptions } from "./model.ts"
|
|
85
|
+
export { decodeSplat, encodeSplat, parseSplat, SPLAT_ATTRIBUTES, SPLAT_RECORD_BYTES, SPLAT_RECORD_TEXELS, SPLAT_SH_TEXELS, SPLAT_TEXEL_BYTES } from "./splat-data.ts"
|
|
86
|
+
export type { ParseSplatOptions, SplatData, SplatFormat } from "./splat-data.ts"
|
|
87
|
+
export { createSplatMesh, loadSplat, splatMaterialClass, splatVertexSource, SPLAT_FRAGMENT, SPLAT_GROUP, SPLAT_ID_ATTRIBUTES } from "./splat.ts"
|
|
88
|
+
export type { SplatMesh as SplatMeshNode, SplatMeshOptions } from "./splat.ts"
|
|
84
89
|
export { bindSkeleton } from "./skeleton.ts"
|
|
85
90
|
export type { BindSkeletonOptions } from "./skeleton.ts"
|
|
86
91
|
export { createMixer } from "./mixer.ts"
|
package/src/light.ts
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import { MAX_CASCADES } from "./glsl.ts"
|
|
6
6
|
import type { Vec3 } from "./math.ts"
|
|
7
|
+
import { checkMask } from "./mesh.ts"
|
|
7
8
|
import { makeNode } from "./node.ts"
|
|
8
9
|
import type { SceneNode } from "./node.ts"
|
|
9
10
|
|
|
11
|
+
// The default light layers mask: every bit set, so a light lights every
|
|
12
|
+
// mesh unless told otherwise (the Unity and Godot default).
|
|
13
|
+
const ALL_LAYERS = 0xffffffff
|
|
14
|
+
|
|
10
15
|
/** The orthographic frustum a casting light renders its shadow map from,
|
|
11
16
|
* in the light's own space (x right, y up, looking along its direction),
|
|
12
17
|
* Three's DirectionalLightShadow camera. Everything outside it is lit. */
|
|
@@ -68,6 +73,10 @@ export type DirectionalLight = SceneNode & {
|
|
|
68
73
|
/** Linear [r, g, b] 0..1. */
|
|
69
74
|
color: Vec3
|
|
70
75
|
intensity: number
|
|
76
|
+
/** Layer filter bitmask (default all bits set): the light lights and
|
|
77
|
+
* shadows only the meshes whose `layers` intersect it - Unity's
|
|
78
|
+
* cullingMask, Godot's light_cull_mask. Write through setLight. */
|
|
79
|
+
layers: number
|
|
71
80
|
/** Render a shadow map from this light (any directional light may;
|
|
72
81
|
* each map is a full extra pass over the casting meshes); meshes with
|
|
73
82
|
* `castShadow` draw into it, `lit` materials read it (unless
|
|
@@ -91,6 +100,9 @@ export type SpotLight = SceneNode & {
|
|
|
91
100
|
/** Linear [r, g, b] 0..1. */
|
|
92
101
|
color: Vec3
|
|
93
102
|
intensity: number
|
|
103
|
+
/** Layer filter bitmask (default all bits set; DirectionalLight.layers
|
|
104
|
+
* is the contract). */
|
|
105
|
+
layers: number
|
|
94
106
|
/** Falloff cutoff in world units; the light is zero past it (0 = no
|
|
95
107
|
* cutoff, Three's rule). */
|
|
96
108
|
distance: number
|
|
@@ -146,6 +158,9 @@ export type PointLight = SceneNode & {
|
|
|
146
158
|
/** Linear [r, g, b] 0..1. */
|
|
147
159
|
color: Vec3
|
|
148
160
|
intensity: number
|
|
161
|
+
/** Layer filter bitmask (default all bits set; DirectionalLight.layers
|
|
162
|
+
* is the contract). */
|
|
163
|
+
layers: number
|
|
149
164
|
/** Falloff cutoff in world units (0 = no cutoff). */
|
|
150
165
|
distance: number
|
|
151
166
|
/** Falloff exponent (default 2). */
|
|
@@ -169,6 +184,10 @@ export type HemisphereLight = SceneNode & {
|
|
|
169
184
|
sky: Vec3
|
|
170
185
|
ground: Vec3
|
|
171
186
|
intensity: number
|
|
187
|
+
/** Layer filter bitmask (default all bits set; DirectionalLight.layers
|
|
188
|
+
* is the contract): a mesh outside it gets no hemisphere term (its own
|
|
189
|
+
* `Surface.ambient` still applies). */
|
|
190
|
+
layers: number
|
|
172
191
|
}
|
|
173
192
|
|
|
174
193
|
export type Light = DirectionalLight | SpotLight | PointLight | HemisphereLight
|
|
@@ -185,6 +204,9 @@ export type DirectionalLightOptions = {
|
|
|
185
204
|
direction?: Vec3
|
|
186
205
|
color?: Vec3
|
|
187
206
|
intensity?: number
|
|
207
|
+
/** Layer filter bitmask, default all bits set (the light lights and
|
|
208
|
+
* shadows only meshes whose `layers` intersect it). */
|
|
209
|
+
layers?: number
|
|
188
210
|
castShadow?: boolean
|
|
189
211
|
/** Shadow-map options, merged key by key (setLight keeps unmentioned ones). */
|
|
190
212
|
shadow?: ShadowOptions
|
|
@@ -193,6 +215,8 @@ export type SpotLightOptions = {
|
|
|
193
215
|
direction?: Vec3
|
|
194
216
|
color?: Vec3
|
|
195
217
|
intensity?: number
|
|
218
|
+
/** Layer filter bitmask, default all bits set. */
|
|
219
|
+
layers?: number
|
|
196
220
|
distance?: number
|
|
197
221
|
angle?: number
|
|
198
222
|
penumbra?: number
|
|
@@ -204,13 +228,21 @@ export type SpotLightOptions = {
|
|
|
204
228
|
export type PointLightOptions = {
|
|
205
229
|
color?: Vec3
|
|
206
230
|
intensity?: number
|
|
231
|
+
/** Layer filter bitmask, default all bits set. */
|
|
232
|
+
layers?: number
|
|
207
233
|
distance?: number
|
|
208
234
|
decay?: number
|
|
209
235
|
castShadow?: boolean
|
|
210
236
|
/** Shadow-map options, merged key by key (setLight keeps unmentioned ones). */
|
|
211
237
|
shadow?: SpotShadowOptions
|
|
212
238
|
}
|
|
213
|
-
export type HemisphereLightOptions = {
|
|
239
|
+
export type HemisphereLightOptions = {
|
|
240
|
+
sky?: Vec3
|
|
241
|
+
ground?: Vec3
|
|
242
|
+
intensity?: number
|
|
243
|
+
/** Layer filter bitmask, default all bits set. */
|
|
244
|
+
layers?: number
|
|
245
|
+
}
|
|
214
246
|
|
|
215
247
|
// Every light may cast; the bound is the shadow-slot budget
|
|
216
248
|
// (MAX_SHADOW_MAPS in glsl.ts, exported from the root with MAX_LIGHTS and
|
|
@@ -248,7 +280,14 @@ function mergeShadow(into: DirectionalLight["shadow"], update: ShadowOptions): v
|
|
|
248
280
|
if (update.bias !== undefined) into.bias = update.bias
|
|
249
281
|
if (update.normalBias !== undefined) into.normalBias = update.normalBias
|
|
250
282
|
if (update.radius !== undefined) into.radius = checkRadius(update.radius)
|
|
251
|
-
if (update.camera !== undefined)
|
|
283
|
+
if (update.camera !== undefined) {
|
|
284
|
+
// Only the keys with a value: an undefined one is absent and keeps
|
|
285
|
+
// the extent, which the light projection reads as is.
|
|
286
|
+
for (let key of Object.keys(update.camera) as (keyof ShadowCamera)[]) {
|
|
287
|
+
let value = update.camera[key]
|
|
288
|
+
if (value !== undefined) into.camera[key] = value
|
|
289
|
+
}
|
|
290
|
+
}
|
|
252
291
|
if (update.cascades !== undefined) {
|
|
253
292
|
let n = update.cascades
|
|
254
293
|
if (!Number.isInteger(n) || n < 1 || n > MAX_CASCADES) throw new Error("shadow.cascades must be an integer from 1 to " + MAX_CASCADES)
|
|
@@ -282,6 +321,7 @@ export function createDirectionalLight(opts: DirectionalLightOptions = {}): Dire
|
|
|
282
321
|
light.direction = [...(opts.direction ?? [0, -1, 0])] as Vec3
|
|
283
322
|
light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
|
|
284
323
|
light.intensity = opts.intensity ?? 1
|
|
324
|
+
light.layers = opts.layers !== undefined ? checkMask(opts.layers, "createDirectionalLight") : ALL_LAYERS
|
|
285
325
|
light.castShadow = opts.castShadow === true
|
|
286
326
|
light.shadow = { mapSize: 1024, bias: 0, normalBias: 0, radius: 1, camera: { left: -5, right: 5, top: 5, bottom: -5, near: 0.5, far: 500 }, cascades: 1, distance: null, splits: null }
|
|
287
327
|
if (opts.shadow !== undefined) mergeShadow(light.shadow, opts.shadow)
|
|
@@ -295,6 +335,7 @@ export function createSpotLight(opts: SpotLightOptions = {}): SpotLight {
|
|
|
295
335
|
light.direction = [...(opts.direction ?? [0, -1, 0])] as Vec3
|
|
296
336
|
light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
|
|
297
337
|
light.intensity = opts.intensity ?? 1
|
|
338
|
+
light.layers = opts.layers !== undefined ? checkMask(opts.layers, "createSpotLight") : ALL_LAYERS
|
|
298
339
|
light.distance = opts.distance ?? 0
|
|
299
340
|
light.angle = opts.angle ?? 60
|
|
300
341
|
light.penumbra = opts.penumbra ?? 0
|
|
@@ -321,6 +362,7 @@ export function createPointLight(opts: PointLightOptions = {}): PointLight {
|
|
|
321
362
|
light.type = "point"
|
|
322
363
|
light.color = [...(opts.color ?? [1, 1, 1])] as Vec3
|
|
323
364
|
light.intensity = opts.intensity ?? 1
|
|
365
|
+
light.layers = opts.layers !== undefined ? checkMask(opts.layers, "createPointLight") : ALL_LAYERS
|
|
324
366
|
light.distance = opts.distance ?? 0
|
|
325
367
|
light.decay = opts.decay ?? 2
|
|
326
368
|
light.castShadow = opts.castShadow === true
|
|
@@ -335,12 +377,13 @@ export function createHemisphereLight(opts: HemisphereLightOptions = {}): Hemisp
|
|
|
335
377
|
light.sky = [...(opts.sky ?? [1, 1, 1])] as Vec3
|
|
336
378
|
light.ground = [...(opts.ground ?? [0.2, 0.2, 0.2])] as Vec3
|
|
337
379
|
light.intensity = opts.intensity ?? 1
|
|
380
|
+
light.layers = opts.layers !== undefined ? checkMask(opts.layers, "createHemisphereLight") : ALL_LAYERS
|
|
338
381
|
return light
|
|
339
382
|
}
|
|
340
383
|
|
|
341
384
|
/** The write path for a light's own fields (color, intensity, direction,
|
|
342
|
-
* the cone and falloff, or sky/ground); absent keys keep their
|
|
343
|
-
* Its placement goes through setTransform like any node.
|
|
385
|
+
* layers, the cone and falloff, or sky/ground); absent keys keep their
|
|
386
|
+
* value. Its placement goes through setTransform like any node.
|
|
344
387
|
* Frame-rate-safe. */
|
|
345
388
|
export function setLight(light: DirectionalLight, update: DirectionalLightOptions): void
|
|
346
389
|
export function setLight(light: SpotLight, update: SpotLightOptions): void
|
|
@@ -351,6 +394,15 @@ export function setLight(
|
|
|
351
394
|
update: DirectionalLightOptions & SpotLightOptions & PointLightOptions & HemisphereLightOptions,
|
|
352
395
|
): void {
|
|
353
396
|
if (update.intensity !== undefined) light.intensity = update.intensity
|
|
397
|
+
if (update.layers !== undefined) {
|
|
398
|
+
let mask = checkMask(update.layers, "setLight")
|
|
399
|
+
if (mask !== light.layers) {
|
|
400
|
+
light.layers = mask
|
|
401
|
+
// A casting light's shadow views re-filter their casters by the new
|
|
402
|
+
// mask (a re-attach, never a tile rebuild - the maps stay).
|
|
403
|
+
light._scene?._lightLayersChanged(light)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
354
406
|
if (light.type === "directional") {
|
|
355
407
|
if (update.direction !== undefined) light.direction = [...update.direction] as Vec3
|
|
356
408
|
if (update.color !== undefined) light.color = [...update.color] as Vec3
|
package/src/material.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
phongFragment,
|
|
53
53
|
litShadowFragment,
|
|
54
54
|
litVertex,
|
|
55
|
+
BLEND_SPACE,
|
|
55
56
|
SKIN_DECLS,
|
|
56
57
|
SKIN_MATRIX,
|
|
57
58
|
standardFragment,
|
|
@@ -140,6 +141,20 @@ export type Material = {
|
|
|
140
141
|
* an entry under a fading LOD level switches hard at the band's
|
|
141
142
|
* midpoint. */
|
|
142
143
|
lodFade?: boolean
|
|
144
|
+
/** True when a stage declares `uLayers` (read from the source, like
|
|
145
|
+
* lodFade; every lit source composing LIGHT_LOOKUP does): the scene
|
|
146
|
+
* then seeds and follows the mesh's `layers` bitmask on entries drawn
|
|
147
|
+
* with this material, which is what lightVector masks lights against.
|
|
148
|
+
* Like the other declared-uniform flags, a source that declares
|
|
149
|
+
* uLayers without using it is rejected at add(). */
|
|
150
|
+
layered?: boolean
|
|
151
|
+
/** True when the fragment declares `uEnv` (read from the source, like
|
|
152
|
+
* layered; every source composing ENVIRONMENT does - `standard`
|
|
153
|
+
* always, `phong` with `reflectivity`): the scene then applies a
|
|
154
|
+
* mesh's own `environment` to entries drawn with this material. On a
|
|
155
|
+
* material without it the mesh's environment is a no-op, never an
|
|
156
|
+
* error - "no reflection" is already true. */
|
|
157
|
+
env?: boolean
|
|
143
158
|
/** Present on materials that own their pipeline (shaderMaterial). */
|
|
144
159
|
dispose?(): void
|
|
145
160
|
}
|
|
@@ -1112,9 +1127,10 @@ export const SKYBOX_FRAGMENT = glsl`
|
|
|
1112
1127
|
uniform samplerCube uSky;
|
|
1113
1128
|
uniform float uSkyIntensity;
|
|
1114
1129
|
uniform mat4 uSkyRotation;
|
|
1130
|
+
${BLEND_SPACE}
|
|
1115
1131
|
void main() {
|
|
1116
1132
|
vec3 dir = mat3(uSkyRotation) * normalize(vRay);
|
|
1117
|
-
fragColor = vec4(max(texture(uSky, dir).rgb * uSkyIntensity, vec3(0.0)), 1.0);
|
|
1133
|
+
fragColor = vec4(blendSpaceOutput(max(texture(uSky, dir).rgb * uSkyIntensity, vec3(0.0)), 1.0), 1.0);
|
|
1118
1134
|
}
|
|
1119
1135
|
`
|
|
1120
1136
|
|
|
@@ -1324,6 +1340,13 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
1324
1340
|
let morphed = /\buMorphs\b/.test(opts.vertex)
|
|
1325
1341
|
// The LOD cross-fade is a fragment-stage discard (Material.lodFade).
|
|
1326
1342
|
let lodFade = /\buLodFade\b/.test(opts.fragment)
|
|
1343
|
+
// A source mentioning uLayers masks lights by the mesh's layers
|
|
1344
|
+
// (Material.layered); either stage may light, so both are scanned.
|
|
1345
|
+
let layered = /\buLayers\b/.test(opts.vertex) || /\buLayers\b/.test(opts.fragment)
|
|
1346
|
+
// A source mentioning uEnv reflects the environment (Material.env; the
|
|
1347
|
+
// \b keeps uEnvIntensity and friends from matching, but ENVIRONMENT
|
|
1348
|
+
// always declares the sampler itself).
|
|
1349
|
+
let env = /\buEnv\b/.test(opts.fragment)
|
|
1327
1350
|
let transparent = opts.transparent ?? (opts.blend !== undefined && opts.blend !== "none")
|
|
1328
1351
|
let cutout = opts.cutout === true
|
|
1329
1352
|
let depth = opts.depth ?? true
|
|
@@ -1424,6 +1447,8 @@ export function shaderMaterialClass(opts: ShaderMaterialClassOptions): ShaderMat
|
|
|
1424
1447
|
skinned,
|
|
1425
1448
|
morphed,
|
|
1426
1449
|
lodFade,
|
|
1450
|
+
layered,
|
|
1451
|
+
env,
|
|
1427
1452
|
attributes,
|
|
1428
1453
|
transparent,
|
|
1429
1454
|
cutout,
|