reze-engine 0.60.1 → 0.60.3
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/README.md +4 -4
- package/dist/engine.d.ts +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +12 -7
- package/dist/graph/compile.d.ts +3 -1
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +1 -1
- package/dist/graph/registry.js +1 -1
- package/dist/graph/render-class.d.ts +3 -0
- package/dist/graph/render-class.d.ts.map +1 -1
- package/dist/graph/slots.d.ts +2 -2
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +21 -10
- package/dist/graph/style-group.d.ts +11 -1
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/model.d.ts +3 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +37 -2
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +70 -3
- package/dist/shaders/passes/scene-contract.d.ts +8 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +3 -0
- package/package.json +1 -1
- package/src/engine.ts +14 -7
- package/src/graph/compile.ts +4 -2
- package/src/graph/registry.ts +1 -1
- package/src/graph/render-class.ts +4 -0
- package/src/graph/slots.ts +23 -10
- package/src/graph/style-group.ts +11 -1
- package/src/index.ts +1 -1
- package/src/model.ts +35 -2
- package/src/shaders/materials/nodes.ts +70 -3
- package/src/shaders/passes/scene-contract.ts +11 -0
package/src/graph/compile.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Diagnostic, ExposedParam, GraphNode, SocketValue, ShaderGraph } fr
|
|
|
8
8
|
import { NODE_REGISTRY, canConvert, convert, fmtValue, literalFits } from "./registry"
|
|
9
9
|
import type { NodeSpec, SockT } from "./registry"
|
|
10
10
|
import { assembleModule } from "./slots"
|
|
11
|
-
import type { AlphaMode, RenderClass } from "./render-class"
|
|
11
|
+
import type { AlphaMode, RenderClass, StyleBlend } from "./render-class"
|
|
12
12
|
|
|
13
13
|
export type CompileOptions = {
|
|
14
14
|
/** Fold exposed params to their defaults as consts (no StyleUniforms binding).
|
|
@@ -22,6 +22,8 @@ export type CompileOptions = {
|
|
|
22
22
|
renderClass?: RenderClass
|
|
23
23
|
/** Alpha-handling axis, orthogonal to renderClass. Default "opaque". */
|
|
24
24
|
alphaMode?: AlphaMode
|
|
25
|
+
/** How the result meets the frame — see StyleGroup.blend. Default "over". */
|
|
26
|
+
blend?: StyleBlend
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** UBO slot for one exposed param: write `value` at style.p[vec4Index] (+ component). */
|
|
@@ -356,6 +358,6 @@ export function compileGraph(graph: ShaderGraph, opts: CompileOptions = {}): Com
|
|
|
356
358
|
if (opacity) lines.push(` let final_opacity = saturate(${outputExpr(opacity, "float")}); // @node:${opacity.node}`)
|
|
357
359
|
|
|
358
360
|
const fsBody = lines.join("\n")
|
|
359
|
-
const wgsl = assembleModule(opts.renderClass ?? "auto", opts.alphaMode ?? "opaque", fsBody, usesStyle.current, !!opacity)
|
|
361
|
+
const wgsl = assembleModule(opts.renderClass ?? "auto", opts.alphaMode ?? "opaque", fsBody, usesStyle.current, !!opacity, opts.blend ?? "over")
|
|
360
362
|
return { ok: true, wgsl, fsBody, slotMap, diagnostics, prunedNodes }
|
|
361
363
|
}
|
package/src/graph/registry.ts
CHANGED
|
@@ -848,7 +848,7 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
848
848
|
const bsdf =
|
|
849
849
|
`eval_principled(PrincipledIn(${a.base_color}, ${a.metallic}, ` +
|
|
850
850
|
`${spec}, ${a.roughness}, ` +
|
|
851
|
-
`${a.spec_clamp}, ${a.sheen_weight}, ${a.sheen_tint}), ${a.normal}, l, v, sun, amb, shadow)`
|
|
851
|
+
`${a.spec_clamp}, ${a.sheen_weight}, ${a.sheen_tint}), ${a.normal}, l, v, sun, amb, shadow, input.worldPos)`
|
|
852
852
|
// v2 defaults Emission Strength to 0, which is the overwhelming case. Emit
|
|
853
853
|
// the term only when it can do something, so the common shader carries no
|
|
854
854
|
// dead add and the output stays readable.
|
|
@@ -11,6 +11,10 @@ export type RenderClass = "auto" | "eye" | "hair"
|
|
|
11
11
|
* hashed alpha test (stockings); "opaque" = the standard near-zero threshold discard. */
|
|
12
12
|
export type AlphaMode = "opaque" | "hashed"
|
|
13
13
|
|
|
14
|
+
/** How a group's colour meets the frame. "over" is the ordinary alpha blend;
|
|
15
|
+
* "additive" adds its light and never reads alpha — see StyleGroup.blend. */
|
|
16
|
+
export type StyleBlend = "over" | "additive"
|
|
17
|
+
|
|
14
18
|
/** Descriptive manifest for hosts (reze-design) so the render-class picker is data-driven
|
|
15
19
|
* instead of hardcoding strings. The effect implementations stay engine-side; this only
|
|
16
20
|
* describes them. `pairsWith` = the effect needs a counterpart class present to show. */
|
package/src/graph/slots.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { NODES_WGSL } from "../shaders/materials/nodes"
|
|
11
11
|
import { COMMON_MATERIAL_PRELUDE_WGSL, DISSOLVE_WGSL, commonFsOutWgsl } from "../shaders/materials/common"
|
|
12
12
|
import { sceneIdWriteWgsl } from "../shaders/passes/scene-contract"
|
|
13
|
-
import type { AlphaMode, RenderClass } from "./render-class"
|
|
13
|
+
import type { AlphaMode, RenderClass, StyleBlend } from "./render-class"
|
|
14
14
|
|
|
15
15
|
// ── Module-scope declarations ──
|
|
16
16
|
// hair: the over-eyes pipeline-override constant (a second pipeline is compiled with
|
|
@@ -80,11 +80,19 @@ function decls(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
|
80
80
|
// fs() header up to and including the graph body's context locals. Composed so the
|
|
81
81
|
// hand-written material shaders' local names (tex_color, n, v, l, sun, amb, shadow) are
|
|
82
82
|
// preserved exactly — the registry's context nodes and emit functions reference them.
|
|
83
|
-
function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
83
|
+
function prelude(renderClass: RenderClass, alphaMode: AlphaMode, blend: StyleBlend): string {
|
|
84
|
+
// ADDITIVE NEVER GATES ON ALPHA. Its blend adds colour and reads no alpha at
|
|
85
|
+
// all, so a surface painted this way carries its picture in RGB and leaves
|
|
86
|
+
// alpha at zero — a game's sky layers do exactly that. Discarding on alpha
|
|
87
|
+
// throws every one of those fragments away before the blend can add them,
|
|
88
|
+
// which is what made X309's nebula and starfield invisible while its cloud
|
|
89
|
+
// dome (opaque) came through.
|
|
84
90
|
const discard =
|
|
85
|
-
|
|
86
|
-
? "
|
|
87
|
-
:
|
|
91
|
+
blend === "additive"
|
|
92
|
+
? ""
|
|
93
|
+
: alphaMode === "hashed"
|
|
94
|
+
? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
|
|
95
|
+
: " if (alpha < 0.001) { discard; }"
|
|
88
96
|
const gate = renderClass === "eye" ? EYE_REAR_GATE : ""
|
|
89
97
|
// Double-sided shading, winding-independent: a normal pointing away from the
|
|
90
98
|
// camera means we're seeing the surface's other side — flip it. Only genuinely
|
|
@@ -143,11 +151,15 @@ ${gate}
|
|
|
143
151
|
// Tail of fs(): consumes `final_color` + locals, writes FSOut. hashed forces output
|
|
144
152
|
// alpha to 1 (the discard already did the cutout); hair scales alpha for the over-eyes
|
|
145
153
|
// pass when IS_OVER_EYES is compiled true.
|
|
146
|
-
function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: boolean): string {
|
|
154
|
+
function epilogue(renderClass: RenderClass, alphaMode: AlphaMode, hasOpacity: boolean, blend: StyleBlend): string {
|
|
147
155
|
// A graph that computes its own opacity wins, including over hashed: hashed
|
|
148
156
|
// writes 1 because its discard already decided the cutout, and a graph asking
|
|
149
|
-
// for a curve is asking for the opposite of a cutout.
|
|
150
|
-
|
|
157
|
+
// for a curve is asking for the opposite of a cutout. Additive writes 1 for a
|
|
158
|
+
// third reason: its colour blend ignores src alpha entirely, so the only thing
|
|
159
|
+
// alpha still reaches is the aux target's coverage — and a sky layer that
|
|
160
|
+
// reported zero coverage would be invisible to bloom and to the composite.
|
|
161
|
+
const alphaBase =
|
|
162
|
+
blend === "additive" ? "1.0" : hasOpacity ? "final_opacity" : alphaMode === "hashed" ? "1.0" : "alpha"
|
|
151
163
|
// Empty while ids are off, so the epilogue is exactly what it was. The values
|
|
152
164
|
// ride in the per-draw material uniform (see MaterialUniforms), which is what
|
|
153
165
|
// keeps this working through the indirect-draw path.
|
|
@@ -202,6 +214,7 @@ export function assembleModule(
|
|
|
202
214
|
fsBody: string,
|
|
203
215
|
includeStyleUniforms: boolean,
|
|
204
216
|
hasOpacity = false,
|
|
217
|
+
blend: StyleBlend = "over",
|
|
205
218
|
): string {
|
|
206
219
|
return (
|
|
207
220
|
NODES_WGSL +
|
|
@@ -212,10 +225,10 @@ export function assembleModule(
|
|
|
212
225
|
commonFsOutWgsl() +
|
|
213
226
|
(includeStyleUniforms ? STYLE_UNIFORMS_WGSL : "") +
|
|
214
227
|
decls(renderClass, alphaMode) +
|
|
215
|
-
prelude(renderClass, alphaMode) +
|
|
228
|
+
prelude(renderClass, alphaMode, blend) +
|
|
216
229
|
fsBody +
|
|
217
230
|
"\n" +
|
|
218
|
-
epilogue(renderClass, alphaMode, hasOpacity) +
|
|
231
|
+
epilogue(renderClass, alphaMode, hasOpacity, blend) +
|
|
219
232
|
"}\n"
|
|
220
233
|
)
|
|
221
234
|
}
|
package/src/graph/style-group.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// the hand-written preset path. See docs/style-groups-spec.md.
|
|
6
6
|
|
|
7
7
|
import type { Diagnostic, ShaderGraph } from "./schema"
|
|
8
|
-
import type { AlphaMode, RenderClass } from "./render-class"
|
|
8
|
+
import type { AlphaMode, RenderClass, StyleBlend } from "./render-class"
|
|
9
9
|
import type { StyleSlot } from "./compile"
|
|
10
10
|
|
|
11
11
|
export type StyleGroup = {
|
|
@@ -22,6 +22,16 @@ export type StyleGroup = {
|
|
|
22
22
|
renderClass?: RenderClass
|
|
23
23
|
/** Alpha-handling axis, orthogonal to renderClass. Default "opaque". */
|
|
24
24
|
alphaMode?: AlphaMode
|
|
25
|
+
/**
|
|
26
|
+
* How this group's colour meets what is already there. Default "over".
|
|
27
|
+
*
|
|
28
|
+
* "additive" is LIGHT rather than matter: the fragment is added to the frame
|
|
29
|
+
* and its alpha is never read. A game's sky layers are painted this way — a
|
|
30
|
+
* nebula and a starfield on cylinders round the scene, their alpha 0 across
|
|
31
|
+
* the whole image because additive blending does not look at it. Laid over,
|
|
32
|
+
* they are invisible.
|
|
33
|
+
*/
|
|
34
|
+
blend?: StyleBlend
|
|
25
35
|
/**
|
|
26
36
|
* Extra image maps for this group's shading, up to four.
|
|
27
37
|
*
|
package/src/index.ts
CHANGED
|
@@ -69,7 +69,7 @@ export type {
|
|
|
69
69
|
Diagnostic,
|
|
70
70
|
} from "./graph/schema"
|
|
71
71
|
export { NODE_REGISTRY, type NodeSpec, type SockT } from "./graph/registry"
|
|
72
|
-
export { RENDER_CLASSES, type RenderClass, type AlphaMode, type RenderClassInfo } from "./graph/render-class"
|
|
72
|
+
export { RENDER_CLASSES, type RenderClass, type AlphaMode, type StyleBlend, type RenderClassInfo } from "./graph/render-class"
|
|
73
73
|
export type {
|
|
74
74
|
StyleGroup,
|
|
75
75
|
GroupImage,
|
package/src/model.ts
CHANGED
|
@@ -1359,11 +1359,44 @@ export class Model {
|
|
|
1359
1359
|
Quat.slerpInto(_eyeOwn, _eyeQuat, w, own)
|
|
1360
1360
|
}
|
|
1361
1361
|
if (bothRot) bothRot.setIdentity()
|
|
1362
|
-
//
|
|
1363
|
-
|
|
1362
|
+
// The eye bones and everything that follows them — the bones under them
|
|
1363
|
+
// and the bones inheriting from them. Rigs often weight the eyeball to one
|
|
1364
|
+
// of those (左目2, a 付与 child), and a motion keying the eyes left it on
|
|
1365
|
+
// the motion's gaze: the full pass had already run.
|
|
1366
|
+
this.eyeSubset ??= this.eyeFollowers(both !== undefined ? [both, left, right] : [left, right])
|
|
1364
1367
|
this.computeWorldMatrices(this.eyeSubset)
|
|
1365
1368
|
}
|
|
1366
1369
|
|
|
1370
|
+
/** The seeds, their descendants and their 付与 inheritors, transitively, in
|
|
1371
|
+
* deform order. */
|
|
1372
|
+
private eyeFollowers(seeds: number[]): Int32Array {
|
|
1373
|
+
const bones = this.skeleton.bones
|
|
1374
|
+
const n = bones.length
|
|
1375
|
+
const affected = new Uint8Array(n)
|
|
1376
|
+
for (const s of seeds) affected[s] = 1
|
|
1377
|
+
let changed = true
|
|
1378
|
+
while (changed) {
|
|
1379
|
+
changed = false
|
|
1380
|
+
for (let k = 0; k < n; k++) {
|
|
1381
|
+
const i = this.deformOrder[k]
|
|
1382
|
+
if (affected[i]) continue
|
|
1383
|
+
const b = bones[i]
|
|
1384
|
+
const ap = b.appendParentIndex
|
|
1385
|
+
const inherits = (b.appendRotate || b.appendMove) && ap !== undefined && ap >= 0 && ap < n && affected[ap]
|
|
1386
|
+
if (inherits || (b.parentIndex >= 0 && affected[b.parentIndex])) {
|
|
1387
|
+
affected[i] = 1
|
|
1388
|
+
changed = true
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const order: number[] = []
|
|
1393
|
+
for (let k = 0; k < n; k++) {
|
|
1394
|
+
const i = this.deformOrder[k]
|
|
1395
|
+
if (affected[i]) order.push(i)
|
|
1396
|
+
}
|
|
1397
|
+
return Int32Array.from(order)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1367
1400
|
/** Compose a constant local rotation onto a bone AFTER every pose source (clip,
|
|
1368
1401
|
* blend, tween) each frame — the classic MMD "heel correction": pitch the 足首
|
|
1369
1402
|
* bones so a motion authored for flat shoes grounds a heeled model. Persists
|
|
@@ -979,6 +979,70 @@ fn principled_specular(ior: f32, level: f32) -> f32 {
|
|
|
979
979
|
return (r * r * 2.0 * max(level, 0.0)) / 0.08;
|
|
980
980
|
}
|
|
981
981
|
|
|
982
|
+
// ─── The scene's lamps, on a PBR closure ───────────────────────────
|
|
983
|
+
//
|
|
984
|
+
// A lamp reaches a material as rzLightsDiffuse, added outside the graph and
|
|
985
|
+
// multiplied by the raw texture: Lambert and nothing else, because that layer
|
|
986
|
+
// cannot know what the surface is. On a principled closure it can. A floor at
|
|
987
|
+
// roughness 0.23 under forty candles carries forty glints, and a metal
|
|
988
|
+
// REFLECTS a lamp instead of having it painted on — without this a ported
|
|
989
|
+
// stage is a photograph of itself, lit but never shining.
|
|
990
|
+
//
|
|
991
|
+
// Same reach as _rzLightOne: the inverse square past RZ_LAMP_NEAR, the
|
|
992
|
+
// (1 − (d/R)⁴)² window that makes the radius real, the squared cone. N·L is
|
|
993
|
+
// inside bsdf_ggx. Declared here for bsdf_ggx and resolved against the lights
|
|
994
|
+
// API the material prelude brings; the ground pass takes that API without
|
|
995
|
+
// this file and is untouched.
|
|
996
|
+
//
|
|
997
|
+
// It walks the light grid a second time — the lamps THIS CELL can see, which
|
|
998
|
+
// is what the grid is for — and with no lamps at all neither walk runs.
|
|
999
|
+
|
|
1000
|
+
fn _rzLampSpecOne(i: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32) -> vec3f {
|
|
1001
|
+
let pr = _rzLightVec(i, 0u);
|
|
1002
|
+
let d = pr.xyz - p;
|
|
1003
|
+
let dist = length(d);
|
|
1004
|
+
if (dist >= pr.w) { return vec3f(0.0); }
|
|
1005
|
+
let toLight = d / max(dist, 1e-4);
|
|
1006
|
+
let ndl = dot(n, toLight);
|
|
1007
|
+
if (ndl <= 0.0) { return vec3f(0.0); }
|
|
1008
|
+
let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
|
|
1009
|
+
let t2 = t * t;
|
|
1010
|
+
let window = 1.0 - t2 * t2;
|
|
1011
|
+
let falloff = window * window / max(dist * dist, RZ_LAMP_NEAR * RZ_LAMP_NEAR);
|
|
1012
|
+
let cone = rzLightCone(i);
|
|
1013
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
1014
|
+
return rzLightColor(i) * (bsdf_ggx(n, toLight, v, ndl, ndv, roughness) * falloff * aim * aim);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
fn _rzLampSpecWord(bits0: u32, base: u32, p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32) -> vec3f {
|
|
1018
|
+
var acc = vec3f(0.0);
|
|
1019
|
+
var bits = bits0;
|
|
1020
|
+
loop {
|
|
1021
|
+
if (bits == 0u) { break; }
|
|
1022
|
+
let i = base + firstTrailingBit(bits);
|
|
1023
|
+
bits = bits & (bits - 1u);
|
|
1024
|
+
acc = acc + _rzLampSpecOne(i, p, n, v, ndv, roughness);
|
|
1025
|
+
}
|
|
1026
|
+
return acc;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
fn rzLampsSpecular(p: vec3f, n: vec3f, v: vec3f, ndv: f32, roughness: f32) -> vec3f {
|
|
1030
|
+
var acc = vec3f(0.0);
|
|
1031
|
+
let count = rzLightCount();
|
|
1032
|
+
let docs = _rzLightDocCount();
|
|
1033
|
+
if (docs > 0u) {
|
|
1034
|
+
let m = _rzLightCellMask(p);
|
|
1035
|
+
acc = acc + _rzLampSpecWord(m.x, 0u, p, n, v, ndv, roughness) +
|
|
1036
|
+
_rzLampSpecWord(m.y, 32u, p, n, v, ndv, roughness) +
|
|
1037
|
+
_rzLampSpecWord(m.z, 64u, p, n, v, ndv, roughness) +
|
|
1038
|
+
_rzLampSpecWord(m.w, 96u, p, n, v, ndv, roughness);
|
|
1039
|
+
}
|
|
1040
|
+
for (var i = docs; i < count; i = i + 1u) {
|
|
1041
|
+
acc = acc + _rzLampSpecOne(i, p, n, v, ndv, roughness);
|
|
1042
|
+
}
|
|
1043
|
+
return acc;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
982
1046
|
struct PrincipledIn {
|
|
983
1047
|
base: vec3f,
|
|
984
1048
|
metallic: f32,
|
|
@@ -992,7 +1056,7 @@ struct PrincipledIn {
|
|
|
992
1056
|
fn eval_principled(
|
|
993
1057
|
p: PrincipledIn,
|
|
994
1058
|
N: vec3f, L: vec3f, V: vec3f,
|
|
995
|
-
sun_rgb: vec3f, amb_rgb: vec3f, shadow: f32
|
|
1059
|
+
sun_rgb: vec3f, amb_rgb: vec3f, shadow: f32, wp: vec3f
|
|
996
1060
|
) -> vec3f {
|
|
997
1061
|
let NL = max(dot(N, L), 0.0);
|
|
998
1062
|
let NV = max(dot(N, V), 1e-4);
|
|
@@ -1011,8 +1075,11 @@ fn eval_principled(
|
|
|
1011
1075
|
// Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after
|
|
1012
1076
|
// accum with reflection_color). ltc_brdf_scale rescales direct to match the
|
|
1013
1077
|
// split-sum indirect path, matching EEVEE closure_eval_glossy_lib behavior.
|
|
1014
|
-
|
|
1015
|
-
|
|
1078
|
+
// The sun, and every lamp that reaches this point. One clamp over the pair:
|
|
1079
|
+
// a candle a hand's width from a polished floor is a firefly otherwise.
|
|
1080
|
+
let spec_direct_raw = (bsdf_ggx(N, L, V, NL, NV, p.roughness) * sun_rgb * shadow
|
|
1081
|
+
+ rzLampsSpecular(wp, N, V, NV, p.roughness))
|
|
1082
|
+
* ltc_brdf_scale_from_lut(lut);
|
|
1016
1083
|
let spec_direct = min(spec_direct_raw, vec3f(p.spec_clamp));
|
|
1017
1084
|
// Indirect specular reads the world ALONG THE REFLECTION, at a roughness-
|
|
1018
1085
|
// picked level — EEVEE's probe_evaluate_world_spec, where the diffuse half
|
|
@@ -90,6 +90,14 @@ type SceneRenderClass =
|
|
|
90
90
|
* cast — a reflection that claimed her object id would seed the distance
|
|
91
91
|
* field twice and put every silhouette effect's border around the glass. */
|
|
92
92
|
| "mirror"
|
|
93
|
+
/** A material whose light is ADDED rather than laid over what is behind it.
|
|
94
|
+
* A game's sky layers are painted this way — a nebula, a starfield, a band of
|
|
95
|
+
* colour on a cylinder round the whole scene — and their alpha is 0 across
|
|
96
|
+
* the entire image, because additive blending never reads it: the picture is
|
|
97
|
+
* wholly in RGB. Drawn alpha-over they are invisible, which is exactly what
|
|
98
|
+
* X309's night sky was. Out of WRITES_ID for the mirror's reason — a sheet of
|
|
99
|
+
* light is not an object anything should be able to pick or outline. */
|
|
100
|
+
| "material-additive"
|
|
93
101
|
/** Particles and ribbons in their default, non-additive mode. */
|
|
94
102
|
| "particle"
|
|
95
103
|
/** Particles declaring `#blend additive` — LIGHT rather than matter, so
|
|
@@ -172,6 +180,9 @@ const WRITES_ID = new Set<SceneRenderClass>(["material", "ground"])
|
|
|
172
180
|
/** The blends each class writes its two attachments with. */
|
|
173
181
|
const BLENDS: Record<Exclude<SceneRenderClass, "depth-prepass">, [GPUBlendState, GPUBlendState]> = {
|
|
174
182
|
material: [ALPHA_OVER, ALPHA_OVER],
|
|
183
|
+
// The same pair the additive particles use: light into the colour target, and
|
|
184
|
+
// coverage that still accumulates so bloom and the composite can see it.
|
|
185
|
+
"material-additive": [ADD_KEEP_ALPHA, ADD_BOTH],
|
|
175
186
|
// PREMULTIPLIED, like the ground and for the same reason: what a mirror
|
|
176
187
|
// writes is a sample of the HDR target, and that target already holds colour
|
|
177
188
|
// premultiplied by its own alpha. Handed to the src-alpha blend it would be
|