@driftengine/core 4.13.5 → 4.14.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.json +62 -0
- package/README.md +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/render/backend/api.d.ts +19 -3
- package/dist/render/backend/webgl2/motionTarget.d.ts +39 -0
- package/dist/render/backend/webgl2/motionTarget.js +254 -0
- package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +13 -3
- package/dist/render/backend/webgl2/pointSkinFeedback.js +45 -12
- package/dist/render/backend/webgl2/renderer.d.ts +101 -7
- package/dist/render/backend/webgl2/renderer.js +337 -9
- package/dist/render/backend/webgpu/pointSkinPass.d.ts +16 -3
- package/dist/render/backend/webgpu/pointSkinPass.js +79 -21
- package/dist/render/backend/webgpu/postPass.d.ts +6 -1
- package/dist/render/backend/webgpu/renderer.d.ts +41 -0
- package/dist/render/backend/webgpu/renderer.js +143 -5
- package/dist/render/baseDecals.d.ts +48 -0
- package/dist/render/baseDecals.js +71 -0
- package/dist/render/emissiveFlow.d.ts +40 -0
- package/dist/render/emissiveFlow.js +51 -0
- package/dist/render/fixtureAtlas.d.ts +8 -3
- package/dist/render/fixtureAtlas.js +17 -6
- package/dist/render/globalMedium.d.ts +17 -0
- package/dist/render/globalMedium.js +14 -0
- package/dist/render/globalMediumPass.js +3 -2
- package/dist/render/iridescentOverlay.d.ts +78 -0
- package/dist/render/iridescentOverlay.js +102 -0
- package/dist/render/mesh.d.ts +24 -0
- package/dist/render/mesh.js +79 -0
- package/dist/render/pointSkin.d.ts +18 -0
- package/dist/render/pointSkin.js +50 -5
- package/dist/render/recon/mover.d.ts +3 -2
- package/dist/render/recon/mover.js +3 -2
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -1
- package/dist/render/shaders/flat/baseDecals.d.ts +17 -0
- package/dist/render/shaders/flat/baseDecals.js +71 -0
- package/dist/render/shaders/flat/emissiveFlow.d.ts +15 -0
- package/dist/render/shaders/flat/emissiveFlow.js +45 -0
- package/dist/render/shaders/flat/glassTint.d.ts +1 -1
- package/dist/render/shaders/flat/glassTint.js +3 -0
- package/dist/render/shaders/flat/index.d.ts +16 -0
- package/dist/render/shaders/flat/index.js +9 -0
- package/dist/render/shaders/flat/iridescentOverlay.d.ts +15 -0
- package/dist/render/shaders/flat/iridescentOverlay.js +65 -0
- package/dist/render/shaders/flat/layered.js +4 -4
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +12 -1
- package/dist/render/shaders/flat/mapReads.d.ts +1 -1
- package/dist/render/shaders/flat/mapReads.js +7 -3
- package/dist/render/shaders/flat/worldUv.js +6 -5
- package/dist/render/shaders/generated/flat.wgsl.d.ts +480 -16
- package/dist/render/shaders/generated/flat.wgsl.js +519 -55
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +15 -5
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +15 -5
- package/dist/render/shaders/temporalResolve.js +25 -9
- package/dist/render/surfaceTexture.d.ts +32 -0
- package/dist/render/temporalPass.d.ts +3 -1
- package/dist/render/temporalPass.js +9 -1
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/render/backend/api.ts +19 -2
- package/src/render/backend/webgl2/motionTarget.ts +299 -0
- package/src/render/backend/webgl2/pointSkinFeedback.ts +49 -14
- package/src/render/backend/webgl2/renderer.ts +411 -9
- package/src/render/backend/webgpu/pointSkinPass.ts +90 -23
- package/src/render/backend/webgpu/renderer.ts +163 -3
- package/src/render/baseDecals.ts +93 -0
- package/src/render/emissiveFlow.ts +66 -0
- package/src/render/fixtureAtlas.ts +26 -7
- package/src/render/globalMedium.ts +31 -0
- package/src/render/globalMediumPass.ts +3 -7
- package/src/render/iridescentOverlay.ts +177 -0
- package/src/render/mesh.ts +82 -0
- package/src/render/pointSkin.ts +64 -5
- package/src/render/recon/mover.ts +3 -2
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/shaders/flat/baseDecals.ts +71 -0
- package/src/render/shaders/flat/emissiveFlow.ts +45 -0
- package/src/render/shaders/flat/glassTint.ts +7 -1
- package/src/render/shaders/flat/index.ts +25 -0
- package/src/render/shaders/flat/iridescentOverlay.ts +65 -0
- package/src/render/shaders/flat/layered.ts +4 -4
- package/src/render/shaders/flat/main.ts +12 -1
- package/src/render/shaders/flat/mapReads.ts +7 -3
- package/src/render/shaders/flat/worldUv.ts +6 -5
- package/src/render/shaders/generated/flat.wgsl.ts +519 -55
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +15 -5
- package/src/render/shaders/temporalResolve.ts +25 -9
- package/src/render/surfaceTexture.ts +32 -0
- package/src/render/temporalPass.ts +8 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
|
|
3
|
+
* packed into `uEmissiveFlow` for the lit stage's `EMISSIVE_FLOW` switch. Both backends pack it
|
|
4
|
+
* here and only upload it, so they cannot disagree about which layer a mask is.
|
|
5
|
+
*
|
|
6
|
+
* **What it is for**: lava in a rock's cracks, a sign's lettering, a glow repeated with a surface's
|
|
7
|
+
* first layer and drifting across it, held to where the surface glows by a map that spans the mesh
|
|
8
|
+
* once. Folded into each vertex's emission instead, the glow spreads across whole faces of a coarse
|
|
9
|
+
* mesh where it should follow the cracks.
|
|
10
|
+
*
|
|
11
|
+
* **The mask is the alpha of a layer of the emissive map's own array**, read at the mesh's first
|
|
12
|
+
* coordinates as they are, before the material's repeat, offset, layers or projection: alpha is
|
|
13
|
+
* never decoded as sRGB, so it is the number it was written as in either colour space, and no
|
|
14
|
+
* texture binding is spent, since the lit stage has none left (`textureUnitBudget.test.ts`). What
|
|
15
|
+
* that gives up is a mask of its own size: a layer of an array is the array's size.
|
|
16
|
+
*
|
|
17
|
+
* **The scroll moves the emissive map alone**, by `Environment.surfaceTime`, in its own repeats a
|
|
18
|
+
* second, wrapped to one so a long-running clock keeps its precision; the albedo and every other map
|
|
19
|
+
* stay where they are.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Floats in `uEmissiveFlow`: the scroll on u and v, the mask's layer plus one (0, none) and one
|
|
23
|
+
* unused; then the scale and offset that take the material's coordinates back to the mesh's own.
|
|
24
|
+
*/
|
|
25
|
+
export declare const EMISSIVE_FLOW_FLOATS = 8;
|
|
26
|
+
/** What a material says about its glow's flow, and the repeat and offset its coordinates carry. */
|
|
27
|
+
export interface EmissiveFlowMaterial {
|
|
28
|
+
readonly emissiveScroll?: readonly [number, number] | null;
|
|
29
|
+
readonly emissiveMaskLayer?: number | null;
|
|
30
|
+
readonly uScale?: number;
|
|
31
|
+
readonly vScale?: number;
|
|
32
|
+
readonly uOffset?: number;
|
|
33
|
+
readonly vOffset?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pack a material's flow into `out` and say whether it asks for one: a scroll that moves, or a mask
|
|
37
|
+
* that is a whole layer. A scroll that is not a number is still; a layer that is not a whole number
|
|
38
|
+
* at or above zero is no mask.
|
|
39
|
+
*/
|
|
40
|
+
export declare function packEmissiveFlow(material: EmissiveFlowMaterial | null, out: Float32Array): boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A material's glow moved and masked: `SurfaceMaterial.emissiveScroll` and `emissiveMaskLayer`,
|
|
3
|
+
* packed into `uEmissiveFlow` for the lit stage's `EMISSIVE_FLOW` switch. Both backends pack it
|
|
4
|
+
* here and only upload it, so they cannot disagree about which layer a mask is.
|
|
5
|
+
*
|
|
6
|
+
* **What it is for**: lava in a rock's cracks, a sign's lettering, a glow repeated with a surface's
|
|
7
|
+
* first layer and drifting across it, held to where the surface glows by a map that spans the mesh
|
|
8
|
+
* once. Folded into each vertex's emission instead, the glow spreads across whole faces of a coarse
|
|
9
|
+
* mesh where it should follow the cracks.
|
|
10
|
+
*
|
|
11
|
+
* **The mask is the alpha of a layer of the emissive map's own array**, read at the mesh's first
|
|
12
|
+
* coordinates as they are, before the material's repeat, offset, layers or projection: alpha is
|
|
13
|
+
* never decoded as sRGB, so it is the number it was written as in either colour space, and no
|
|
14
|
+
* texture binding is spent, since the lit stage has none left (`textureUnitBudget.test.ts`). What
|
|
15
|
+
* that gives up is a mask of its own size: a layer of an array is the array's size.
|
|
16
|
+
*
|
|
17
|
+
* **The scroll moves the emissive map alone**, by `Environment.surfaceTime`, in its own repeats a
|
|
18
|
+
* second, wrapped to one so a long-running clock keeps its precision; the albedo and every other map
|
|
19
|
+
* stay where they are.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Floats in `uEmissiveFlow`: the scroll on u and v, the mask's layer plus one (0, none) and one
|
|
23
|
+
* unused; then the scale and offset that take the material's coordinates back to the mesh's own.
|
|
24
|
+
*/
|
|
25
|
+
export const EMISSIVE_FLOW_FLOATS = 8;
|
|
26
|
+
/**
|
|
27
|
+
* Pack a material's flow into `out` and say whether it asks for one: a scroll that moves, or a mask
|
|
28
|
+
* that is a whole layer. A scroll that is not a number is still; a layer that is not a whole number
|
|
29
|
+
* at or above zero is no mask.
|
|
30
|
+
*/
|
|
31
|
+
export function packEmissiveFlow(material, out) {
|
|
32
|
+
const scroll = material?.emissiveScroll ?? null;
|
|
33
|
+
const u = scroll !== null && Number.isFinite(scroll[0]) ? scroll[0] : 0;
|
|
34
|
+
const v = scroll !== null && Number.isFinite(scroll[1]) ? scroll[1] : 0;
|
|
35
|
+
const layer = material?.emissiveMaskLayer ?? null;
|
|
36
|
+
const masked = layer !== null && Number.isInteger(layer) && layer >= 0;
|
|
37
|
+
out[0] = u;
|
|
38
|
+
out[1] = v;
|
|
39
|
+
/* Plus one, so an unwritten zero is no mask rather than a mask of layer 0. */
|
|
40
|
+
out[2] = masked ? layer + 1 : 0;
|
|
41
|
+
out[3] = 0;
|
|
42
|
+
/* The vertex stage laid `uv · scale + offset`; this undoes it. A scale of zero has no inverse,
|
|
43
|
+
and every texel of such a surface is the one coordinate, so the mesh's own is taken as 0. */
|
|
44
|
+
const uScale = material?.uScale ?? 1;
|
|
45
|
+
const vScale = material?.vScale ?? 1;
|
|
46
|
+
out[4] = uScale === 0 ? 0 : 1 / uScale;
|
|
47
|
+
out[5] = vScale === 0 ? 0 : 1 / vScale;
|
|
48
|
+
out[6] = uScale === 0 ? 0 : -(material?.uOffset ?? 0) / uScale;
|
|
49
|
+
out[7] = vScale === 0 ? 0 : -(material?.vOffset ?? 0) / vScale;
|
|
50
|
+
return u !== 0 || v !== 0 || masked;
|
|
51
|
+
}
|
|
@@ -3,11 +3,16 @@ export interface FixtureAtlasLayout {
|
|
|
3
3
|
readonly height: number;
|
|
4
4
|
/** Rows of cookie tiles at the top: `COOKIE_TILE`, or 0 with none loaded. */
|
|
5
5
|
readonly cookieRows: number;
|
|
6
|
-
/** The first row of the
|
|
6
|
+
/** The first row of the decals' images, below the cookies, and how many rows they take. */
|
|
7
|
+
readonly decalTop: number;
|
|
8
|
+
readonly decalRows: number;
|
|
9
|
+
/** The first row of the profiles, below the cookies and the decals. */
|
|
7
10
|
readonly iesTop: number;
|
|
8
11
|
}
|
|
9
|
-
/** The
|
|
10
|
-
export declare function
|
|
12
|
+
/** The row the decals' band starts at: below the cookies, which are there or are not. */
|
|
13
|
+
export declare function decalBandTop(cookieTiles: number): number;
|
|
14
|
+
/** The atlas for `iesRows` rows of profile, `cookieTiles` cookies and `decalTiles` decal images. */
|
|
15
|
+
export declare function fixtureAtlasLayout(iesRows: number, cookieTiles: number, decalTiles?: number): FixtureAtlasLayout;
|
|
11
16
|
/**
|
|
12
17
|
* The profiles' rows as the atlas holds them: `data` one float a texel, `width` across and `rows`
|
|
13
18
|
* down, spread into the red channel of a four-channel image the atlas's size, the rest ones. The
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a light fixture throws, in one texture: its spot cookies in the top band, its photometric
|
|
3
|
-
* profiles in the rows below.
|
|
3
|
+
* profiles in the rows below. And between the two, since the lit stage had no binding left for them
|
|
4
|
+
* either, the images of the decals laid into base colours (`baseDecals.ts`), a band of squares a
|
|
5
|
+
* frame may rewrite; each band is found from its own end or its own row, so the others' lookups are
|
|
6
|
+
* as they were.
|
|
4
7
|
*
|
|
5
8
|
* **One binding for two tables, because the lit stage had one sampler left.** The cookie atlas and
|
|
6
9
|
* the photometric atlas were two small float textures read with `textureLod`, and the widest lit
|
|
@@ -19,14 +22,22 @@
|
|
|
19
22
|
*/
|
|
20
23
|
import { IES_ATLAS_WIDTH } from './iesProfile.js';
|
|
21
24
|
import { COOKIE_TILE } from './lightBudget.js';
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
import { BASE_DECAL_TILE } from './baseDecals.js';
|
|
26
|
+
/** The row the decals' band starts at: below the cookies, which are there or are not. */
|
|
27
|
+
export function decalBandTop(cookieTiles) {
|
|
28
|
+
return cookieTiles > 0 ? COOKIE_TILE : 0;
|
|
29
|
+
}
|
|
30
|
+
/** The atlas for `iesRows` rows of profile, `cookieTiles` cookies and `decalTiles` decal images. */
|
|
31
|
+
export function fixtureAtlasLayout(iesRows, cookieTiles, decalTiles = 0) {
|
|
24
32
|
const cookieRows = cookieTiles > 0 ? COOKIE_TILE : 0;
|
|
33
|
+
const decalRows = decalTiles > 0 ? BASE_DECAL_TILE : 0;
|
|
25
34
|
return {
|
|
26
|
-
width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles),
|
|
27
|
-
height: cookieRows + iesRows,
|
|
35
|
+
width: Math.max(IES_ATLAS_WIDTH, COOKIE_TILE * cookieTiles, BASE_DECAL_TILE * decalTiles),
|
|
36
|
+
height: cookieRows + decalRows + iesRows,
|
|
28
37
|
cookieRows,
|
|
29
|
-
|
|
38
|
+
decalTop: cookieRows,
|
|
39
|
+
decalRows,
|
|
40
|
+
iesTop: cookieRows + decalRows,
|
|
30
41
|
};
|
|
31
42
|
}
|
|
32
43
|
/**
|
|
@@ -110,6 +110,16 @@ export interface GlobalMediumOptions {
|
|
|
110
110
|
readonly sunScatter?: number;
|
|
111
111
|
/** The same for the sky's fill, which lights a shadowed medium from every side. Default 1. */
|
|
112
112
|
readonly skyScatter?: number;
|
|
113
|
+
/**
|
|
114
|
+
* The sky's fill the medium scatters, a colour of its own in the units of
|
|
115
|
+
* `Environment.ambient`, which `skyScatter` then scales. Absent or null, the frame's ambient, as
|
|
116
|
+
* it always was. **For a scene whose surfaces are lit by more than its flat ambient**, a sky's
|
|
117
|
+
* cubemap, probes and a bake, which keeps that ambient for what falls outside them: the ambient
|
|
118
|
+
* is then the wrong colour for the sky's light in the air, and setting it to the right one would
|
|
119
|
+
* relight those surfaces too. What it gives up is the sky's direction, as the ambient does: one
|
|
120
|
+
* colour from every side.
|
|
121
|
+
*/
|
|
122
|
+
readonly skyFill?: readonly [number, number, number] | null;
|
|
113
123
|
/**
|
|
114
124
|
* The same for the lamps, each through its own shadow and times its own `lightMediumScatter`.
|
|
115
125
|
* **Default 0**: a medium was the sun's and the sky's until there was this, and a scene that
|
|
@@ -135,6 +145,8 @@ export interface ResolvedGlobalMedium {
|
|
|
135
145
|
readonly sunScatter: number;
|
|
136
146
|
readonly skyScatter: number;
|
|
137
147
|
readonly lampScatter: number;
|
|
148
|
+
/** The sky's fill of the medium's own, or null for the frame's ambient: see `mediumSkyFill`. */
|
|
149
|
+
readonly skyFill: readonly [number, number, number] | null;
|
|
138
150
|
}
|
|
139
151
|
/**
|
|
140
152
|
* What a renderer starts with: off, and everything else at a value that means "ordinary air".
|
|
@@ -153,6 +165,11 @@ export declare const DEFAULT_GLOBAL_MEDIUM: GlobalMediumOptions;
|
|
|
153
165
|
* than reporting; a `maxDistance` of zero is a march with no length that still costs its steps.
|
|
154
166
|
*/
|
|
155
167
|
export declare function resolveGlobalMedium(options: Partial<GlobalMediumOptions>): ResolvedGlobalMedium;
|
|
168
|
+
/**
|
|
169
|
+
* The colour the medium scatters as the sky's fill: its own where it has one, else the frame's
|
|
170
|
+
* ambient. One decision both backends upload, so the two cannot disagree about which.
|
|
171
|
+
*/
|
|
172
|
+
export declare function mediumSkyFill(medium: Pick<ResolvedGlobalMedium, 'skyFill'>, ambient: ArrayLike<number>): ArrayLike<number>;
|
|
156
173
|
/** `setGlobalMedium`'s object form: a density, and any of the rest. */
|
|
157
174
|
export type GlobalMediumSettings = Partial<GlobalMediumOptions> & {
|
|
158
175
|
readonly density: number;
|
|
@@ -88,8 +88,22 @@ export function resolveGlobalMedium(options) {
|
|
|
88
88
|
sunScatter: Math.max(0, finite(options.sunScatter, 1)),
|
|
89
89
|
skyScatter: Math.max(0, finite(options.skyScatter, 1)),
|
|
90
90
|
lampScatter: Math.max(0, finite(options.lampScatter, 0)),
|
|
91
|
+
skyFill: options.skyFill === undefined || options.skyFill === null
|
|
92
|
+
? null
|
|
93
|
+
: [
|
|
94
|
+
Math.max(0, finite(options.skyFill[0], 0)),
|
|
95
|
+
Math.max(0, finite(options.skyFill[1], 0)),
|
|
96
|
+
Math.max(0, finite(options.skyFill[2], 0)),
|
|
97
|
+
],
|
|
91
98
|
};
|
|
92
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The colour the medium scatters as the sky's fill: its own where it has one, else the frame's
|
|
102
|
+
* ambient. One decision both backends upload, so the two cannot disagree about which.
|
|
103
|
+
*/
|
|
104
|
+
export function mediumSkyFill(medium, ambient) {
|
|
105
|
+
return medium.skyFill ?? ambient;
|
|
106
|
+
}
|
|
93
107
|
/**
|
|
94
108
|
* `setGlobalMedium`'s two forms, its four numbers or the object, read one way for both backends:
|
|
95
109
|
* a density that is not a number is none.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mat4 } from 'gl-matrix';
|
|
2
|
-
import { mediumTargetSize } from './globalMedium.js';
|
|
2
|
+
import { mediumSkyFill, mediumTargetSize } from './globalMedium.js';
|
|
3
3
|
import { DEPTH_01_TO_CLIP } from './lightVolumeDraw.js';
|
|
4
4
|
import { compileProgram, uniformLocations } from './shader.js';
|
|
5
5
|
import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
|
|
@@ -168,7 +168,8 @@ export class GlobalMediumPass {
|
|
|
168
168
|
gl.uniform3f(u['uCameraPos'] ?? null, eye[0] ?? 0, eye[1] ?? 0, eye[2] ?? 0);
|
|
169
169
|
gl.uniform3f(u['uSunDir'] ?? null, light.sunDir[0] ?? 0, light.sunDir[1] ?? 1, light.sunDir[2] ?? 0);
|
|
170
170
|
gl.uniform3f(u['uSunColor'] ?? null, light.sunColor[0] ?? 0, light.sunColor[1] ?? 0, light.sunColor[2] ?? 0);
|
|
171
|
-
|
|
171
|
+
const sky = mediumSkyFill(options, light.ambient);
|
|
172
|
+
gl.uniform3f(u['uAmbient'] ?? null, sky[0] ?? 0, sky[1] ?? 0, sky[2] ?? 0);
|
|
172
173
|
gl.uniform1f(u['uDensity'] ?? null, options.density);
|
|
173
174
|
gl.uniform2f(u['uHeight'] ?? null, options.heightFalloff, options.baseY);
|
|
174
175
|
gl.uniform3f(u['uLayer2'] ?? null, options.density2, options.heightFalloff2, options.baseY2);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A surface's iridescent overlay: a hue that shifts with the view, laid over the base colour before
|
|
3
|
+
* it is lit. `SurfaceMaterial.iridescentOverlay`, packed into `uIridescence` for the lit stage's
|
|
4
|
+
* `IRIDESCENT_OVERLAY` switch, and the same arithmetic on the CPU, which the tests hold the shader to.
|
|
5
|
+
*
|
|
6
|
+
* Per pixel, with `N` the mapped normal, `V` toward the eye and `uv` the mesh's own first
|
|
7
|
+
* coordinates, as many material systems shade a sheen on cloth or a coat:
|
|
8
|
+
*
|
|
9
|
+
* h = saturate((N·H)^contrast), 0 where N·H ≤ 0, H = normalize(V + direction)
|
|
10
|
+
* x = N·V + uv.x + blend · (h − N·V)
|
|
11
|
+
* t = saturate(fract(x · tiling))
|
|
12
|
+
* base' = base + (overlay(base, ramp(t)) − base) · x · mask · weight
|
|
13
|
+
*
|
|
14
|
+
* where `overlay` is the usual blend, `a < ½ ? 2ao : 1 − 2(1 − a)(1 − o)` per channel.
|
|
15
|
+
*
|
|
16
|
+
* **The ramp is uniforms, thirty-two colours, and not a texture**, because the lit stage has no
|
|
17
|
+
* texture binding left (`textureUnitBudget.test.ts`): a caller's ramp of any length is resampled to
|
|
18
|
+
* thirty-two on the way in and read back between neighbours. What that gives up is a ramp with
|
|
19
|
+
* detail finer than a thirty-second of its length, which a smooth spectrum does not have. **The
|
|
20
|
+
* mask is a channel of a layer of one of the material's own arrays**, for the same reason, read at
|
|
21
|
+
* the material's coordinates; alpha is linear in every array, and a colour channel of an sRGB array
|
|
22
|
+
* is decoded before it is read.
|
|
23
|
+
*
|
|
24
|
+
* Thirty-six vectors, so WebGL2 refuses it, in words, on a part whose lit stage has no room, as it
|
|
25
|
+
* refuses a material's tiled detail.
|
|
26
|
+
*/
|
|
27
|
+
/** Three numbers, read and never written. */
|
|
28
|
+
type Three = readonly [number, number, number];
|
|
29
|
+
/** Colours the ramp is resampled to. */
|
|
30
|
+
export declare const IRIDESCENT_RAMP = 32;
|
|
31
|
+
/** Floats in `uIridescence`: four vectors of numbers, then the ramp a vector a colour. */
|
|
32
|
+
export declare const IRIDESCENT_FLOATS: number;
|
|
33
|
+
/** Which of a material's arrays holds the mask. */
|
|
34
|
+
export type IridescentMaskMap = 'orm' | 'model' | 'albedo' | 'emissive';
|
|
35
|
+
/** `SurfaceMaterial.iridescentOverlay`. */
|
|
36
|
+
export interface IridescentOverlay {
|
|
37
|
+
/**
|
|
38
|
+
* The overlay colour along `t`, linear, three floats a colour, at least two colours, the first at
|
|
39
|
+
* `t = 0` and the last at `t = 1`. Resampled to thirty-two.
|
|
40
|
+
*/
|
|
41
|
+
readonly ramp: ArrayLike<number>;
|
|
42
|
+
/** A fixed direction in the world the sheen turns toward, `S`. Need not be unit length. */
|
|
43
|
+
readonly direction: Three;
|
|
44
|
+
/** How far the view's term is turned toward the sheen, 0 to 1. Default 0.6. */
|
|
45
|
+
readonly blend?: number;
|
|
46
|
+
/** The sheen's exponent. Default 1. */
|
|
47
|
+
readonly contrast?: number;
|
|
48
|
+
/** How many times the ramp repeats across `x`. Default 2. */
|
|
49
|
+
readonly tiling?: number;
|
|
50
|
+
/** How much of the overlay is laid on. Default 0.25. */
|
|
51
|
+
readonly weight?: number;
|
|
52
|
+
/** Where it is laid: a channel of a layer of one of the material's arrays. Absent, everywhere. */
|
|
53
|
+
readonly mask?: {
|
|
54
|
+
readonly map: IridescentMaskMap;
|
|
55
|
+
readonly layer: number;
|
|
56
|
+
/** 0 red, 1 green, 2 blue, 3 alpha. */
|
|
57
|
+
readonly channel: 0 | 1 | 2 | 3;
|
|
58
|
+
} | null;
|
|
59
|
+
}
|
|
60
|
+
/** Why an overlay cannot be drawn, or null where it can. */
|
|
61
|
+
export declare function iridescentRefusal(overlay: IridescentOverlay): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Pack an overlay into `out`, with the scale and offset that take the material's coordinates back
|
|
64
|
+
* to the mesh's own; all zeros, which the shader reads as none, for null. Refuse what
|
|
65
|
+
* `iridescentRefusal` refuses before calling this.
|
|
66
|
+
*/
|
|
67
|
+
export declare function packIridescentOverlay(overlay: IridescentOverlay | null, uv: {
|
|
68
|
+
uScale?: number;
|
|
69
|
+
vScale?: number;
|
|
70
|
+
uOffset?: number;
|
|
71
|
+
vOffset?: number;
|
|
72
|
+
}, out: Float32Array): void;
|
|
73
|
+
/**
|
|
74
|
+
* The base colour with the overlay laid on, as the lit stage computes it from a packed block: `n`
|
|
75
|
+
* and `toEye` unit length, `uv` the mesh's own coordinates, `mask` the texel's channel. Into `out`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function iridescentBase(packed: ArrayLike<number>, base: Three, n: Three, toEye: Three, uv: readonly [number, number], mask: number, out: [number, number, number]): [number, number, number];
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Colours the ramp is resampled to. */
|
|
2
|
+
export const IRIDESCENT_RAMP = 32;
|
|
3
|
+
/** Floats in `uIridescence`: four vectors of numbers, then the ramp a vector a colour. */
|
|
4
|
+
export const IRIDESCENT_FLOATS = (4 + IRIDESCENT_RAMP) * 4;
|
|
5
|
+
const MASK_SOURCE = {
|
|
6
|
+
orm: 1,
|
|
7
|
+
model: 2,
|
|
8
|
+
albedo: 3,
|
|
9
|
+
emissive: 4,
|
|
10
|
+
};
|
|
11
|
+
/** Why an overlay cannot be drawn, or null where it can. */
|
|
12
|
+
export function iridescentRefusal(overlay) {
|
|
13
|
+
const values = overlay.ramp.length;
|
|
14
|
+
if (values < 6 || values % 3 !== 0) {
|
|
15
|
+
return `a ramp of ${values} numbers; it is three a colour, at least two colours`;
|
|
16
|
+
}
|
|
17
|
+
const [x, y, z] = overlay.direction;
|
|
18
|
+
if (!(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) ||
|
|
19
|
+
x * x + y * y + z * z === 0) {
|
|
20
|
+
return 'a direction that is not one';
|
|
21
|
+
}
|
|
22
|
+
const mask = overlay.mask ?? null;
|
|
23
|
+
if (mask !== null && !(Number.isInteger(mask.layer) && mask.layer >= 0)) {
|
|
24
|
+
return `a mask layer of ${mask.layer}; a layer is a whole number from 0`;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pack an overlay into `out`, with the scale and offset that take the material's coordinates back
|
|
30
|
+
* to the mesh's own; all zeros, which the shader reads as none, for null. Refuse what
|
|
31
|
+
* `iridescentRefusal` refuses before calling this.
|
|
32
|
+
*/
|
|
33
|
+
export function packIridescentOverlay(overlay, uv, out) {
|
|
34
|
+
out.fill(0);
|
|
35
|
+
if (overlay === null)
|
|
36
|
+
return;
|
|
37
|
+
const [x, y, z] = overlay.direction;
|
|
38
|
+
const length = Math.hypot(x, y, z);
|
|
39
|
+
out[0] = x / length;
|
|
40
|
+
out[1] = y / length;
|
|
41
|
+
out[2] = z / length;
|
|
42
|
+
out[3] = overlay.blend ?? 0.6;
|
|
43
|
+
out[4] = overlay.contrast ?? 1;
|
|
44
|
+
out[5] = overlay.tiling ?? 2;
|
|
45
|
+
out[6] = overlay.weight ?? 0.25;
|
|
46
|
+
/* On: a zero here, which an unwritten block is, is none. */
|
|
47
|
+
out[7] = 1;
|
|
48
|
+
const mask = overlay.mask ?? null;
|
|
49
|
+
out[8] = mask === null ? 0 : MASK_SOURCE[mask.map];
|
|
50
|
+
out[9] = mask === null ? 0 : mask.layer;
|
|
51
|
+
out[10] = mask === null ? 0 : mask.channel;
|
|
52
|
+
const uScale = uv.uScale ?? 1;
|
|
53
|
+
const vScale = uv.vScale ?? 1;
|
|
54
|
+
out[12] = uScale === 0 ? 0 : 1 / uScale;
|
|
55
|
+
out[13] = vScale === 0 ? 0 : 1 / vScale;
|
|
56
|
+
out[14] = uScale === 0 ? 0 : -(uv.uOffset ?? 0) / uScale;
|
|
57
|
+
out[15] = vScale === 0 ? 0 : -(uv.vOffset ?? 0) / vScale;
|
|
58
|
+
/* The ramp, resampled to thirty-two colours between its own neighbours. */
|
|
59
|
+
const colours = overlay.ramp.length / 3;
|
|
60
|
+
for (let i = 0; i < IRIDESCENT_RAMP; i++) {
|
|
61
|
+
const at = (i / (IRIDESCENT_RAMP - 1)) * (colours - 1);
|
|
62
|
+
const below = Math.min(Math.floor(at), colours - 2);
|
|
63
|
+
const share = at - below;
|
|
64
|
+
for (let c = 0; c < 3; c++) {
|
|
65
|
+
const a = overlay.ramp[below * 3 + c];
|
|
66
|
+
const b = overlay.ramp[(below + 1) * 3 + c];
|
|
67
|
+
out[16 + i * 4 + c] = a + (b - a) * share;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The base colour with the overlay laid on, as the lit stage computes it from a packed block: `n`
|
|
73
|
+
* and `toEye` unit length, `uv` the mesh's own coordinates, `mask` the texel's channel. Into `out`.
|
|
74
|
+
*/
|
|
75
|
+
export function iridescentBase(packed, base, n, toEye, uv, mask, out) {
|
|
76
|
+
const p = (i) => packed[i];
|
|
77
|
+
let hx = toEye[0] + p(0);
|
|
78
|
+
let hy = toEye[1] + p(1);
|
|
79
|
+
let hz = toEye[2] + p(2);
|
|
80
|
+
const hl = Math.hypot(hx, hy, hz) || 1;
|
|
81
|
+
hx /= hl;
|
|
82
|
+
hy /= hl;
|
|
83
|
+
hz /= hl;
|
|
84
|
+
const nh = n[0] * hx + n[1] * hy + n[2] * hz;
|
|
85
|
+
const h = nh > 0 ? Math.min(1, Math.max(0, nh ** p(4))) : 0;
|
|
86
|
+
const nv = n[0] * toEye[0] + n[1] * toEye[1] + n[2] * toEye[2];
|
|
87
|
+
const x = nv + uv[0] + p(3) * (h - nv);
|
|
88
|
+
const scaled = x * p(5);
|
|
89
|
+
const t = Math.min(1, Math.max(0, scaled - Math.floor(scaled)));
|
|
90
|
+
const at = t * (IRIDESCENT_RAMP - 1);
|
|
91
|
+
const below = Math.min(Math.floor(at), IRIDESCENT_RAMP - 2);
|
|
92
|
+
const share = at - below;
|
|
93
|
+
const amount = x * mask * p(6);
|
|
94
|
+
for (let c = 0; c < 3; c++) {
|
|
95
|
+
const o0 = p(16 + below * 4 + c);
|
|
96
|
+
const o = o0 + (p(16 + (below + 1) * 4 + c) - o0) * share;
|
|
97
|
+
const a = base[c];
|
|
98
|
+
const blended = a < 0.5 ? 2 * a * o : 1 - 2 * (1 - a) * (1 - o);
|
|
99
|
+
out[c] = a + (blended - a) * amount;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
package/dist/render/mesh.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export declare class Mesh {
|
|
|
22
22
|
readonly positions: WebGLBuffer;
|
|
23
23
|
readonly normals: WebGLBuffer;
|
|
24
24
|
} | null;
|
|
25
|
+
/**
|
|
26
|
+
* A dynamic mesh's positions as they stood before this frame's first rewrite, for the motion
|
|
27
|
+
* WebGL2's temporal resolve follows (`backend/webgl2/motionTarget.ts`). Made the first time one is
|
|
28
|
+
* kept, so a mesh that never moves under the resolve pays nothing.
|
|
29
|
+
*/
|
|
30
|
+
private previousPositions;
|
|
31
|
+
/** The frame that rewrite belongs to (`frameOfChange`), or -1 for none yet. */
|
|
32
|
+
changedFrame: number;
|
|
33
|
+
/** A vertex array for the motion pass, without and with last frame's positions, made on demand. */
|
|
34
|
+
private motionArrays;
|
|
25
35
|
/** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
|
|
26
36
|
private readonly recolourable;
|
|
27
37
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
@@ -140,6 +150,20 @@ export declare class Mesh {
|
|
|
140
150
|
/** Draw `count` instances through a batch's own vertex array. See `createInstanceArray`. */
|
|
141
151
|
drawInstancesThrough(gl: WebGL2RenderingContext, vao: WebGLVertexArrayObject, count: number): void;
|
|
142
152
|
dispose(gl: WebGL2RenderingContext): void;
|
|
153
|
+
/**
|
|
154
|
+
* Keep this dynamic mesh's positions as they stand, before a rewrite: one buffer copy on the
|
|
155
|
+
* device. The caller decides whether this rewrite is the frame's first (`keepsPrevious`).
|
|
156
|
+
*/
|
|
157
|
+
keepPrevious(gl: WebGL2RenderingContext): void;
|
|
158
|
+
/** Whether last frame's positions have been kept: a dynamic mesh rewritten under the resolve. */
|
|
159
|
+
get keptPrevious(): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Draw this mesh for the motion pass: its positions at 0, last frame's at 1 where `previous` asks
|
|
162
|
+
* and they were kept, and its rig's influences where `flat`'s skinning reads them, 11, 12, 14 and
|
|
163
|
+
* 15. A vertex array of its own, made once a variant, so the one the lit pass draws with is never
|
|
164
|
+
* touched.
|
|
165
|
+
*/
|
|
166
|
+
drawMotion(gl: WebGL2RenderingContext, previous: boolean): void;
|
|
143
167
|
/**
|
|
144
168
|
* Rewrite this mesh's positions, and its normals where the caller has them.
|
|
145
169
|
*
|
package/dist/render/mesh.js
CHANGED
|
@@ -27,6 +27,8 @@ import { validateMeshData } from '@driftengine/drft';
|
|
|
27
27
|
*/
|
|
28
28
|
/** Attribute locations are fixed engine-wide (`layout(location = n)` in GLSL). */
|
|
29
29
|
const ATTR_POSITION = 0;
|
|
30
|
+
/** What the motion pass reads: positions, and a rig's two sets of joints and weights. */
|
|
31
|
+
const MOTION_LOCATIONS = [ATTR_POSITION, 11, 12, 14, 15];
|
|
30
32
|
const ATTR_NORMAL = 1;
|
|
31
33
|
const ATTR_COLOR = 2;
|
|
32
34
|
const ATTR_EMISSIVE = 3;
|
|
@@ -96,6 +98,16 @@ export class Mesh {
|
|
|
96
98
|
get streams() {
|
|
97
99
|
return this.deformable;
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* A dynamic mesh's positions as they stood before this frame's first rewrite, for the motion
|
|
103
|
+
* WebGL2's temporal resolve follows (`backend/webgl2/motionTarget.ts`). Made the first time one is
|
|
104
|
+
* kept, so a mesh that never moves under the resolve pays nothing.
|
|
105
|
+
*/
|
|
106
|
+
previousPositions = null;
|
|
107
|
+
/** The frame that rewrite belongs to (`frameOfChange`), or -1 for none yet. */
|
|
108
|
+
changedFrame = -1;
|
|
109
|
+
/** A vertex array for the motion pass, without and with last frame's positions, made on demand. */
|
|
110
|
+
motionArrays = [null, null];
|
|
99
111
|
/** The colour buffer, kept so `updateColors` can rewrite it. Null unless `dynamicColors`. */
|
|
100
112
|
recolourable = null;
|
|
101
113
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
@@ -426,6 +438,73 @@ export class Mesh {
|
|
|
426
438
|
for (const buffer of this.buffers)
|
|
427
439
|
gl.deleteBuffer(buffer);
|
|
428
440
|
gl.deleteVertexArray(this.vao);
|
|
441
|
+
for (const vao of this.motionArrays)
|
|
442
|
+
if (vao !== null)
|
|
443
|
+
gl.deleteVertexArray(vao);
|
|
444
|
+
if (this.previousPositions !== null)
|
|
445
|
+
gl.deleteBuffer(this.previousPositions);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Keep this dynamic mesh's positions as they stand, before a rewrite: one buffer copy on the
|
|
449
|
+
* device. The caller decides whether this rewrite is the frame's first (`keepsPrevious`).
|
|
450
|
+
*/
|
|
451
|
+
keepPrevious(gl) {
|
|
452
|
+
const deformable = this.deformable;
|
|
453
|
+
if (deformable === null)
|
|
454
|
+
return;
|
|
455
|
+
const bytes = this.positionFloats * 4;
|
|
456
|
+
if (this.previousPositions === null) {
|
|
457
|
+
const buffer = gl.createBuffer();
|
|
458
|
+
if (buffer === null)
|
|
459
|
+
throw new Error('createBuffer failed');
|
|
460
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, buffer);
|
|
461
|
+
gl.bufferData(gl.COPY_WRITE_BUFFER, bytes, gl.DYNAMIC_COPY);
|
|
462
|
+
this.previousPositions = buffer;
|
|
463
|
+
}
|
|
464
|
+
gl.bindBuffer(gl.COPY_READ_BUFFER, deformable.positions);
|
|
465
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, this.previousPositions);
|
|
466
|
+
gl.copyBufferSubData(gl.COPY_READ_BUFFER, gl.COPY_WRITE_BUFFER, 0, 0, bytes);
|
|
467
|
+
gl.bindBuffer(gl.COPY_READ_BUFFER, null);
|
|
468
|
+
gl.bindBuffer(gl.COPY_WRITE_BUFFER, null);
|
|
469
|
+
}
|
|
470
|
+
/** Whether last frame's positions have been kept: a dynamic mesh rewritten under the resolve. */
|
|
471
|
+
get keptPrevious() {
|
|
472
|
+
return this.previousPositions !== null;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Draw this mesh for the motion pass: its positions at 0, last frame's at 1 where `previous` asks
|
|
476
|
+
* and they were kept, and its rig's influences where `flat`'s skinning reads them, 11, 12, 14 and
|
|
477
|
+
* 15. A vertex array of its own, made once a variant, so the one the lit pass draws with is never
|
|
478
|
+
* touched.
|
|
479
|
+
*/
|
|
480
|
+
drawMotion(gl, previous) {
|
|
481
|
+
const at = previous && this.previousPositions !== null ? 1 : 0;
|
|
482
|
+
let vao = this.motionArrays[at] ?? null;
|
|
483
|
+
if (vao === null) {
|
|
484
|
+
vao = gl.createVertexArray();
|
|
485
|
+
if (vao === null)
|
|
486
|
+
throw new Error('createVertexArray failed');
|
|
487
|
+
gl.bindVertexArray(vao);
|
|
488
|
+
for (const attribute of this.layout) {
|
|
489
|
+
if (!MOTION_LOCATIONS.includes(attribute.location))
|
|
490
|
+
continue;
|
|
491
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, attribute.buffer);
|
|
492
|
+
gl.enableVertexAttribArray(attribute.location);
|
|
493
|
+
gl.vertexAttribPointer(attribute.location, attribute.size, attribute.type, attribute.normalized, 0, 0);
|
|
494
|
+
}
|
|
495
|
+
if (at === 1 && this.previousPositions !== null) {
|
|
496
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.previousPositions);
|
|
497
|
+
gl.enableVertexAttribArray(1);
|
|
498
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
|
|
499
|
+
}
|
|
500
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
|
|
501
|
+
gl.bindVertexArray(null);
|
|
502
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, null);
|
|
503
|
+
this.motionArrays[at] = vao;
|
|
504
|
+
}
|
|
505
|
+
gl.bindVertexArray(vao);
|
|
506
|
+
gl.drawElements(gl.TRIANGLES, this.indexCount, this.indexType, 0);
|
|
507
|
+
gl.bindVertexArray(null);
|
|
429
508
|
}
|
|
430
509
|
/**
|
|
431
510
|
* Rewrite this mesh's positions, and its normals where the caller has them.
|
|
@@ -44,8 +44,26 @@ export interface PointSkinReach {
|
|
|
44
44
|
}
|
|
45
45
|
/** Refuse a skin whose arrays disagree with a mesh of `vertices`, or that names a joint past the rest. */
|
|
46
46
|
export declare function validatePointSkin(data: PointSkinData, vertices: number): void;
|
|
47
|
+
/**
|
|
48
|
+
* Refuse a rewrite of vertices `first` onward, as many as `data` holds, that does not fit a mesh of
|
|
49
|
+
* `vertices`: arrays that disagree with each other, a range that is not whole vertices inside the
|
|
50
|
+
* mesh, or a joint past the rest. Named by the mesh's own vertex numbers.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validatePointSkinRange(data: PointSkinData, first: number, vertices: number): void;
|
|
47
53
|
/** Measure a skin's reach once, at creation. */
|
|
48
54
|
export declare function reachOf(data: PointSkinData): PointSkinReach;
|
|
55
|
+
/**
|
|
56
|
+
* A skin's reach after part of it is rewritten with `data`: the farther of the old reach and the
|
|
57
|
+
* new at each joint, the joints the two name between them, and the widest span of weight sums.
|
|
58
|
+
*
|
|
59
|
+
* **It widens and never narrows**, because the vertices the rewrite replaced may have been the
|
|
60
|
+
* ones that set a joint's reach, and finding out would mean keeping every vertex on the CPU and
|
|
61
|
+
* reading them all again. A skin that changes shape back and forth settles at the union of its
|
|
62
|
+
* shapes, which is still a box no posed vertex leaves, and culls a little less than it could. What
|
|
63
|
+
* would make that wrong is a skin rewritten into ever larger shapes and never back; making it
|
|
64
|
+
* again with `createPointSkin` measures it afresh.
|
|
65
|
+
*/
|
|
66
|
+
export declare function widenReach(reach: PointSkinReach, data: PointSkinData): PointSkinReach;
|
|
49
67
|
/**
|
|
50
68
|
* The bounds of a pose, into the mesh's own: `matrices` holds each joint's transform into the
|
|
51
69
|
* mesh's model space, sixteen floats, column-major. Allocates nothing.
|