reze-engine 0.66.3 → 0.66.5
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/dist/engine.d.ts +55 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +101 -3
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +15 -0
- 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 +75 -0
- package/dist/shaders/passes/outline.d.ts +6 -0
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +17 -4
- package/package.json +1 -1
- package/src/engine.ts +100 -3
- package/src/graph/registry.ts +15 -0
- package/src/shaders/materials/nodes.ts +75 -0
- package/src/shaders/passes/outline.ts +17 -4
package/src/engine.ts
CHANGED
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
} from "./shaders/lights"
|
|
80
80
|
import { buildLightGrid } from "./light-grid"
|
|
81
81
|
import { groundShaderWgsl, GROUND_NOISE_BAKE_WGSL, GROUND_NOISE_SIZE } from "./shaders/passes/ground"
|
|
82
|
-
import { outlineShaderWgsl, RZ_OUTLINE_DISSOLVE_OFFSET } from "./shaders/passes/outline"
|
|
82
|
+
import { outlineShaderWgsl, RZ_OUTLINE_COLOR_OFFSET, RZ_OUTLINE_DISSOLVE_OFFSET, RZ_OUTLINE_WIDTH_OFFSET } from "./shaders/passes/outline"
|
|
83
83
|
import { transparentDepthPrepassWgsl } from "./shaders/passes/depth-prepass"
|
|
84
84
|
import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shaders/passes/selection"
|
|
85
85
|
import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo"
|
|
@@ -3324,6 +3324,51 @@ export class Engine {
|
|
|
3324
3324
|
this.bundlesDirty = true
|
|
3325
3325
|
}
|
|
3326
3326
|
|
|
3327
|
+
/**
|
|
3328
|
+
* Every outline's width, as a multiple of what each PMX material asked for
|
|
3329
|
+
* (its edgeSize): 1 is the author's own, 0 draws none. A multiplier rather
|
|
3330
|
+
* than a width because a model's lines are not one width — the hair's rim
|
|
3331
|
+
* and a sleeve's crease differ on purpose, and scaling keeps that.
|
|
3332
|
+
*
|
|
3333
|
+
* A write, not a re-record: one float in each hull's uniform, so a slider
|
|
3334
|
+
* can drive it. Hulls built later take the current value at creation.
|
|
3335
|
+
*/
|
|
3336
|
+
private outlineWidth = 1
|
|
3337
|
+
setOutlineWidth(scale: number): void {
|
|
3338
|
+
const v = Number.isFinite(scale) ? Math.max(0, scale) : 1
|
|
3339
|
+
if (this.outlineWidth === v) return
|
|
3340
|
+
this.outlineWidth = v
|
|
3341
|
+
if (!this.device) return
|
|
3342
|
+
const one = new Float32Array([v])
|
|
3343
|
+
for (const inst of this.modelInstances.values()) {
|
|
3344
|
+
for (const buffer of inst.outlineUniformBuffers) {
|
|
3345
|
+
this.device.queue.writeBuffer(buffer, RZ_OUTLINE_WIDTH_OFFSET, one)
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* One colour for every outline, in place of each PMX material's edge colour;
|
|
3352
|
+
* null hands each material its own back. Written as the edge colour is —
|
|
3353
|
+
* the values a PMX would carry — so a picked black is MMD's black. The
|
|
3354
|
+
* material's edge ALPHA is kept: only the hue is the scene's.
|
|
3355
|
+
*
|
|
3356
|
+
* A write, like setOutlineWidth: hulls built later take it at creation.
|
|
3357
|
+
*/
|
|
3358
|
+
private outlineColor: [number, number, number, number] = [0, 0, 0, 0]
|
|
3359
|
+
setOutlineColor(color: Vec3 | { x: number; y: number; z: number } | null): void {
|
|
3360
|
+
const next: [number, number, number, number] = color ? [color.x, color.y, color.z, 1] : [0, 0, 0, 0]
|
|
3361
|
+
if (next.every((v, i) => v === this.outlineColor[i])) return
|
|
3362
|
+
this.outlineColor = next
|
|
3363
|
+
if (!this.device) return
|
|
3364
|
+
const data = new Float32Array(next)
|
|
3365
|
+
for (const inst of this.modelInstances.values()) {
|
|
3366
|
+
for (const buffer of inst.outlineUniformBuffers) {
|
|
3367
|
+
this.device.queue.writeBuffer(buffer, RZ_OUTLINE_COLOR_OFFSET, data)
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3327
3372
|
/**
|
|
3328
3373
|
* Draw the id attachment instead of the scene.
|
|
3329
3374
|
*
|
|
@@ -9981,6 +10026,46 @@ export class Engine {
|
|
|
9981
10026
|
if (this.cameraPoseOverride) this.camera.setVmdPose(this.cameraPoseOverride)
|
|
9982
10027
|
}
|
|
9983
10028
|
|
|
10029
|
+
/**
|
|
10030
|
+
* The host's adjustment to a camera track, added to every pose it samples.
|
|
10031
|
+
*
|
|
10032
|
+
* A downloaded camera motion is framed for whoever it was made with: a
|
|
10033
|
+
* shorter model wants it lower, a wider stage wants it further back. This
|
|
10034
|
+
* moves the shot without touching its ANGLE — the motion's rotation and roll
|
|
10035
|
+
* are its choreography, and they stay the track's.
|
|
10036
|
+
*
|
|
10037
|
+
* - `target`: world units, added to the look-at point, so the whole shot
|
|
10038
|
+
* slides with the angle unchanged.
|
|
10039
|
+
* - `distance`: positive pulls the eye further back along its own line of
|
|
10040
|
+
* sight. VMD distances are signed (negative is behind the target, the usual
|
|
10041
|
+
* case), so "further" means away from zero on whichever side it is.
|
|
10042
|
+
* - `fov`: radians, added to the track's lens, never below a sliver.
|
|
10043
|
+
*
|
|
10044
|
+
* Only a loaded track reads it: the orbit has its own sliders, and a pose
|
|
10045
|
+
* forced through setCameraPose is a statement of where the camera IS.
|
|
10046
|
+
*/
|
|
10047
|
+
private cameraTrackOffset = { target: new Vec3(0, 0, 0), distance: 0, fov: 0 }
|
|
10048
|
+
setCameraTrackOffset(offset: { target?: { x: number; y: number; z: number }; distance?: number; fov?: number } | null): void {
|
|
10049
|
+
const t = offset?.target
|
|
10050
|
+
this.cameraTrackOffset = {
|
|
10051
|
+
target: new Vec3(t?.x ?? 0, t?.y ?? 0, t?.z ?? 0),
|
|
10052
|
+
distance: offset?.distance ?? 0,
|
|
10053
|
+
fov: offset?.fov ?? 0,
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10056
|
+
|
|
10057
|
+
/** A sampled track pose with the host's offset laid on. */
|
|
10058
|
+
private offsetTrackPose(pose: CameraPose): CameraPose {
|
|
10059
|
+
const o = this.cameraTrackOffset
|
|
10060
|
+
if (o.distance === 0 && o.fov === 0 && o.target.x === 0 && o.target.y === 0 && o.target.z === 0) return pose
|
|
10061
|
+
return {
|
|
10062
|
+
target: new Vec3(pose.target.x + o.target.x, pose.target.y + o.target.y, pose.target.z + o.target.z),
|
|
10063
|
+
rotation: pose.rotation,
|
|
10064
|
+
distance: pose.distance + Math.sign(pose.distance || -1) * o.distance,
|
|
10065
|
+
fov: Math.min(Math.PI * 0.95, Math.max(0.01, pose.fov + o.fov)),
|
|
10066
|
+
}
|
|
10067
|
+
}
|
|
10068
|
+
|
|
9984
10069
|
/** The pose currently forced from outside, or null when nothing is. */
|
|
9985
10070
|
getCameraPoseOverride(): CameraPose | null {
|
|
9986
10071
|
return this.cameraPoseOverride
|
|
@@ -10974,6 +11059,12 @@ export class Engine {
|
|
|
10974
11059
|
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
10975
11060
|
],
|
|
10976
11061
|
})
|
|
11062
|
+
// The recorded phases bind these groups by value (sceneView), so a rebuild
|
|
11063
|
+
// they never hear about leaves them drawing with the old one: the first
|
|
11064
|
+
// light cookie landed on the ground (re-bound each frame) and not on the
|
|
11065
|
+
// cast, and a swapped sky kept reflecting the one it replaced. Only init,
|
|
11066
|
+
// a world install and the first cookie get here, so re-recording is cheap.
|
|
11067
|
+
this.bundlesDirty = true
|
|
10977
11068
|
return true
|
|
10978
11069
|
}
|
|
10979
11070
|
|
|
@@ -15458,8 +15549,14 @@ export class Engine {
|
|
|
15458
15549
|
// block's layout would be 32 bytes of padding per outlined material
|
|
15459
15550
|
// to carry one float. See RZ_OUTLINE_DISSOLVE_OFFSET.
|
|
15460
15551
|
1,
|
|
15552
|
+
// The scene's width multiplier, at RZ_OUTLINE_WIDTH_OFFSET.
|
|
15553
|
+
this.outlineWidth,
|
|
15461
15554
|
0,
|
|
15462
|
-
|
|
15555
|
+
// The scene's colour override, at RZ_OUTLINE_COLOR_OFFSET.
|
|
15556
|
+
this.outlineColor[0],
|
|
15557
|
+
this.outlineColor[1],
|
|
15558
|
+
this.outlineColor[2],
|
|
15559
|
+
this.outlineColor[3],
|
|
15463
15560
|
])
|
|
15464
15561
|
const outlineUniformBuffer = this.createUniformBuffer(`${prefix}outline: ${mat.name}`, materialUniformData)
|
|
15465
15562
|
inst.gpuBuffers.push(outlineUniformBuffer)
|
|
@@ -17035,7 +17132,7 @@ export class Engine {
|
|
|
17035
17132
|
} else if (this.camera.vmdDriven && this.cameraAnimation) {
|
|
17036
17133
|
// Drive the shot from the camera VMD (synced to the animated model's clock).
|
|
17037
17134
|
const pose = this.cameraAnimation.sample(this.transportTime())
|
|
17038
|
-
if (pose) this.camera.setVmdPose(pose)
|
|
17135
|
+
if (pose) this.camera.setVmdPose(this.offsetTrackPose(pose))
|
|
17039
17136
|
}
|
|
17040
17137
|
|
|
17041
17138
|
// Before the cast is written, which carries every subject's dissolve to the
|
package/src/graph/registry.ts
CHANGED
|
@@ -683,6 +683,21 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
|
|
|
683
683
|
emit: (a) => `rz_glossy_direct(${a.normal}, l, v, sun, shadow, input.worldPos, 1.0 - (${a.smoothness}))`,
|
|
684
684
|
takesLight: true,
|
|
685
685
|
},
|
|
686
|
+
/**
|
|
687
|
+
* The lamps, handed to the graph — Shader Graph's Additional Lights: their
|
|
688
|
+
* diffuse at `normal`, and a Blinn-Phong highlight pow(N·H, exponent), both
|
|
689
|
+
* untinted. A graph that reads them combines them as its shader does (the
|
|
690
|
+
* game's water adds the diffuse into its body before its colour, and glints
|
|
691
|
+
* each lamp at exponent 128·smoothness), and owns the lamps' diffuse: the
|
|
692
|
+
* layer the engine otherwise adds after the graph is not added.
|
|
693
|
+
*/
|
|
694
|
+
additional_lights: {
|
|
695
|
+
inputs: { exponent: F(64), normal: { type: "vector", contextDefault: "n" } },
|
|
696
|
+
outputs: { diffuse: "color", specular: "color" },
|
|
697
|
+
outputSelect: { diffuse: ".d", specular: ".s" },
|
|
698
|
+
emit: (a) => `rz_additional_lights(input.worldPos, ${a.normal}, v, ${a.exponent})`,
|
|
699
|
+
takesLight: true,
|
|
700
|
+
},
|
|
686
701
|
reflection_probe: {
|
|
687
702
|
inputs: {
|
|
688
703
|
vector: { type: "vector", contextDefault: "reflect(-v, n)" },
|
|
@@ -1193,6 +1193,81 @@ fn rzLampsSpecular(p: vec3f, n: vec3f, v: vec3f, roughness: f32) -> vec3f {
|
|
|
1193
1193
|
return s;
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
+
// ─── Additional lights, handed to the graph ─────────────
|
|
1197
|
+
//
|
|
1198
|
+
// Shader Graph's "Additional Lights" idiom: every lamp's diffuse and a
|
|
1199
|
+
// Blinn-Phong highlight, pow(N·H, e), for the graph to combine the way its
|
|
1200
|
+
// shader does. The game's water adds the diffuse into its body BEFORE tinting
|
|
1201
|
+
// it by its own colour and glints each lamp on that lobe — neither of which
|
|
1202
|
+
// the epilogue's layer (lamps × the slot-0 picture, added after) can say. So a
|
|
1203
|
+
// graph that reads them owns the lamps' diffuse and the epilogue adds none.
|
|
1204
|
+
//
|
|
1205
|
+
// Same reach as _rzLightOne: bulb, window, squared cone, cookie. A point
|
|
1206
|
+
// behind the surface gets neither term, as the game's step(N·L ≥ 0) has it.
|
|
1207
|
+
|
|
1208
|
+
fn _rzAddLightOne(i: u32, p: vec3f, n: vec3f, v: vec3f, e: f32) -> _RzLampPair {
|
|
1209
|
+
var out = _RzLampPair(vec3f(0.0), vec3f(0.0));
|
|
1210
|
+
if (!_rzLightReaches(i)) { return out; }
|
|
1211
|
+
let pr = _rzLightVec(i, 0u);
|
|
1212
|
+
let d = pr.xyz - p;
|
|
1213
|
+
let dist = length(d);
|
|
1214
|
+
if (dist >= pr.w) { return out; }
|
|
1215
|
+
let toLight = d / max(dist, 1e-4);
|
|
1216
|
+
let ndl = dot(n, toLight);
|
|
1217
|
+
if (ndl < 0.0) { return out; }
|
|
1218
|
+
let t = clamp(dist / max(pr.w, 1e-4), 0.0, 1.0);
|
|
1219
|
+
let t2 = t * t;
|
|
1220
|
+
let window = 1.0 - t2 * t2;
|
|
1221
|
+
let bulb = rzLightNear(i);
|
|
1222
|
+
let falloff = window * window / max(dist * dist, bulb * bulb);
|
|
1223
|
+
let cone = rzLightCone(i);
|
|
1224
|
+
let aim = clamp((dot(-toLight, rzLightAim(i)) - cone.x) / max(cone.y - cone.x, 1e-4), 0.0, 1.0);
|
|
1225
|
+
let reach = rzLightColor(i) * rzLightCookie(i, toLight) * (falloff * aim * aim);
|
|
1226
|
+
out.d = reach * ndl;
|
|
1227
|
+
out.s = reach * pow(saturate(dot(n, normalize(toLight + v))), e);
|
|
1228
|
+
return out;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
fn _rzAddLightWord(bits0: u32, base: u32, p: vec3f, n: vec3f, v: vec3f, e: f32) -> _RzLampPair {
|
|
1232
|
+
var acc = _RzLampPair(vec3f(0.0), vec3f(0.0));
|
|
1233
|
+
var bits = bits0;
|
|
1234
|
+
loop {
|
|
1235
|
+
if (bits == 0u) { break; }
|
|
1236
|
+
let i = base + firstTrailingBit(bits);
|
|
1237
|
+
bits = bits & (bits - 1u);
|
|
1238
|
+
let one = _rzAddLightOne(i, p, n, v, e);
|
|
1239
|
+
acc.d = acc.d + one.d;
|
|
1240
|
+
acc.s = acc.s + one.s;
|
|
1241
|
+
}
|
|
1242
|
+
return acc;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
fn rz_additional_lights(p: vec3f, n: vec3f, v: vec3f, e: f32) -> _RzLampPair {
|
|
1246
|
+
var acc = _RzLampPair(vec3f(0.0), vec3f(0.0));
|
|
1247
|
+
let count = rzLightCount();
|
|
1248
|
+
let docs = _rzLightDocCount();
|
|
1249
|
+
if (docs > 0u) {
|
|
1250
|
+
let m = _rzLightCellMask(p);
|
|
1251
|
+
let w0 = _rzAddLightWord(m.lo.x, 0u, p, n, v, e);
|
|
1252
|
+
let w1 = _rzAddLightWord(m.lo.y, 32u, p, n, v, e);
|
|
1253
|
+
let w2 = _rzAddLightWord(m.lo.z, 64u, p, n, v, e);
|
|
1254
|
+
let w3 = _rzAddLightWord(m.lo.w, 96u, p, n, v, e);
|
|
1255
|
+
let w4 = _rzAddLightWord(m.hi.x, 128u, p, n, v, e);
|
|
1256
|
+
let w5 = _rzAddLightWord(m.hi.y, 160u, p, n, v, e);
|
|
1257
|
+
let w6 = _rzAddLightWord(m.hi.z, 192u, p, n, v, e);
|
|
1258
|
+
let w7 = _rzAddLightWord(m.hi.w, 224u, p, n, v, e);
|
|
1259
|
+
acc.d = w0.d + w1.d + w2.d + w3.d + w4.d + w5.d + w6.d + w7.d;
|
|
1260
|
+
acc.s = w0.s + w1.s + w2.s + w3.s + w4.s + w5.s + w6.s + w7.s;
|
|
1261
|
+
}
|
|
1262
|
+
for (var i = docs; i < count; i = i + 1u) {
|
|
1263
|
+
let one = _rzAddLightOne(i, p, n, v, e);
|
|
1264
|
+
acc.d = acc.d + one.d;
|
|
1265
|
+
acc.s = acc.s + one.s;
|
|
1266
|
+
}
|
|
1267
|
+
_rzLampDiffuseTaken = true;
|
|
1268
|
+
return acc;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1196
1271
|
// ─── Lit: URP's PBR, term for term ─────────────
|
|
1197
1272
|
//
|
|
1198
1273
|
// What Unity's Lit shader (URP's UniversalFragmentPBR) computes, so a
|
|
@@ -27,6 +27,12 @@ import { DISSOLVE_WGSL } from "../materials/common"
|
|
|
27
27
|
* edgeSize 16..20, so this is 20.
|
|
28
28
|
*/
|
|
29
29
|
export const RZ_OUTLINE_DISSOLVE_OFFSET = 20
|
|
30
|
+
/** Where the scene-wide width multiplier sits, in bytes — the float after
|
|
31
|
+
* dissolve (Engine.setOutlineWidth). */
|
|
32
|
+
export const RZ_OUTLINE_WIDTH_OFFSET = 24
|
|
33
|
+
/** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
|
|
34
|
+
* for the material's own edge colour (Engine.setOutlineColor). */
|
|
35
|
+
export const RZ_OUTLINE_COLOR_OFFSET = 32
|
|
30
36
|
|
|
31
37
|
export function outlineShaderWgsl(): string {
|
|
32
38
|
return /* wgsl */ `
|
|
@@ -40,7 +46,7 @@ struct CameraUniforms {
|
|
|
40
46
|
viewportHeight: f32,
|
|
41
47
|
};
|
|
42
48
|
|
|
43
|
-
// THE HULL'S OWN BLOCK, not the material's.
|
|
49
|
+
// THE HULL'S OWN BLOCK, not the material's. 48 bytes of edge data, which is all
|
|
44
50
|
// this pass shades with — and dissolve, which it has to obey.
|
|
45
51
|
//
|
|
46
52
|
// It reached for the material block's own layout first, declaring the skipped
|
|
@@ -53,8 +59,14 @@ struct MaterialUniforms {
|
|
|
53
59
|
edgeColor: vec4f,
|
|
54
60
|
edgeSize: f32,
|
|
55
61
|
dissolve: f32,
|
|
56
|
-
|
|
62
|
+
/** The scene's width multiplier (setOutlineWidth), the same on every hull:
|
|
63
|
+
* scales the author's edgeSize rather than replacing it, so a model's thin
|
|
64
|
+
* and thick lines keep their proportion. */
|
|
65
|
+
widthScale: f32,
|
|
57
66
|
_padding3: f32,
|
|
67
|
+
/** The scene's colour in place of edgeColor.rgb, by .a (0 or 1). The
|
|
68
|
+
* material's own alpha still holds — a line its author made sheer stays so. */
|
|
69
|
+
colorOverride: vec4f,
|
|
58
70
|
};
|
|
59
71
|
|
|
60
72
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
@@ -158,7 +170,7 @@ const RZ_OUTLINE_MIN_PX = 1.5;
|
|
|
158
170
|
// multiplies it all, as vertex colour alpha does in the game.
|
|
159
171
|
let w = max(clipPos.w, 1e-4);
|
|
160
172
|
let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
|
|
161
|
-
let nearNdc = viewNormal.xy * (material.edgeSize * outlineNormal.w * 4.0 / refViewport);
|
|
173
|
+
let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
|
|
162
174
|
var ndc = nearNdc * (min(w, breakDepth) / w);
|
|
163
175
|
|
|
164
176
|
// Antialiased minimum (the game's _OutlineAntialias): held at the minimum
|
|
@@ -212,7 +224,8 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
|
|
|
212
224
|
// alpha above), it is drawn right after the surface it traces so what lies
|
|
213
225
|
// under a faded line is already there, and 4× MSAA would quantize a fade to
|
|
214
226
|
// four steps.
|
|
215
|
-
|
|
227
|
+
let rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
|
|
228
|
+
out.color = vec4f(rgb, material.edgeColor.a * texA * input.coverage);
|
|
216
229
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
217
230
|
${sceneIdPadWgsl("out")} return out;
|
|
218
231
|
}
|