reze-engine 0.50.12 → 0.51.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/dist/engine.js CHANGED
@@ -2,7 +2,7 @@ import { Camera } from "./camera";
2
2
  import { decodeDds, isDds } from "./dds-loader";
3
3
  import { Mat4, Quat, Vec3 } from "./math";
4
4
  import { decodePsd, isPsd } from "./psd-loader";
5
- import { MATERIAL_MORPH_MULTIPLY } from "./model";
5
+ import { Model, MATERIAL_MORPH_MULTIPLY } from "./model";
6
6
  import { MORPH_COMPUTE_WGSL } from "./shaders/passes/morph";
7
7
  import { CULL_COMPUTE_WGSL } from "./shaders/passes/cull";
8
8
  import { buildAnchorTable, anchorAliasWgsl, EMPTY_ANCHOR_TABLE } from "./shaders/anchor-table";
@@ -41,6 +41,7 @@ import { PICK_SHADER_WGSL } from "./shaders/passes/pick";
41
41
  import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap";
42
42
  import { compileGraph } from "./graph/compile";
43
43
  import { DEFAULT_GRAPH } from "./graph/presets/default";
44
+ import { UNLIT_GRAPH } from "./graph/presets/unlit";
44
45
  import { FACE_GRAPH } from "./graph/presets/face";
45
46
  import { HAIR_GRAPH } from "./graph/presets/hair";
46
47
  import { BODY_GRAPH } from "./graph/presets/body";
@@ -983,6 +984,8 @@ export class Engine {
983
984
  this.framePhysicsMsRaw = 0;
984
985
  /** Every shadow caster in one sphere: (x, y, z, radius). radius 0 = nothing
985
986
  * casts, -1 = do not use (a rigid caster has no sphere). See updateCasterSphere. */
987
+ /** A card's own texture, by model key — see setPlaneFrame. */
988
+ this.planeTextures = new Map();
986
989
  this.casterSphere = new Float32Array(4);
987
990
  /** The ground's uniform block, kept so the caster sphere can be refreshed in
988
991
  * it every frame rather than rebuilding the buffer (addGround allocates). */
@@ -2040,41 +2043,6 @@ export class Engine {
2040
2043
  primitive: { topology: "triangle-list" },
2041
2044
  });
2042
2045
  }
2043
- /**
2044
- * Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
2045
- * composite pass. ONE effect per scene, and the code says where it mounts by
2046
- * which of these it defines — either, or both in one file:
2047
- *
2048
- * fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
2049
- * fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f
2050
- *
2051
- * `background` is a LAYER between the base background and the scene,
2052
- * over-composited onto whichever base is active (solid color, 360 equirect, or
2053
- * transparency) — its alpha lets the base show through, so a starfield is
2054
- * stars over the user's background color. `foreground` composites over the
2055
- * finished frame instead, which is where rain, snow, petals and fog live, and
2056
- * is handed `depth`: the camera-space distance in metres of whatever the scene
2057
- * drew at that pixel (the far plane where it drew nothing). Compare a
2058
- * particle's own distance against it and the model occludes it; fog just reads
2059
- * it, since fog's alpha IS a function of distance.
2060
- *
2061
- * `ray` is the pixel's normalized world-space view direction (LH, +Z forward —
2062
- * what the skybox samples by), `uv` is 0..1 bottom-left origin, `time` is
2063
- * seconds since apply, and `bgResolution()` gives the canvas size. Return sRGB
2064
- * + alpha; alpha is the only "how much does this replace" control there is.
2065
- * Declared `params` arrive as `params.<name>` (number → f32, Vec3 → vec3f),
2066
- * shared by both mounts, and are later tweaked without recompiling via
2067
- * setEffectParam.
2068
- *
2069
- * Both mounts are display-space: neither affects lighting, bloom or
2070
- * tonemapping, and both are captured by offline export. A foreground makes the
2071
- * scene pass STORE its depth buffer (it otherwise discards it into tile
2072
- * memory) for as long as one is installed.
2073
- *
2074
- * Compiles off the hot path (async pipelines): on failure the previous effect
2075
- * is KEPT and diagnostics are returned with line numbers relative to the
2076
- * user's WGSL. Pass null to remove the effect.
2077
- */
2078
2046
  async compileEffect(wgsl, params,
2079
2047
  /** This effect's own declarations, already parsed by the caller — which had
2080
2048
  * to read them anyway to build the scene table. */
@@ -2170,6 +2138,28 @@ export class Engine {
2170
2138
  // pinning an effect that declares this has to keep installing; saying so is
2171
2139
  // all that was ever missing.
2172
2140
  const warnings = [];
2141
+ // A DIRECTIVE THAT PARSED AS NOTHING.
2142
+ //
2143
+ // Every pragma is matched with `\s*$` after it, so a line that carries a
2144
+ // note as well — `// @fullres — glyph edges are sub-pixel detail` — matches
2145
+ // none of them and is read as an ordinary comment. Nothing failed, nothing
2146
+ // said anything, and the effect simply ran without the property it asked
2147
+ // for: three shipped effects were silently half-res and a fourth silently
2148
+ // stopped being additive, each one's first line explaining why it needed
2149
+ // the thing it was not getting.
2150
+ //
2151
+ // Every directive this engine knows, so an unrecognised one is named rather
2152
+ // than ignored. Cheap: it runs once per install, over a file a human wrote.
2153
+ for (const m of wgsl.matchAll(/^[ \t]*\/\/[ \t]*(@[a-zA-Z]+)(.*)$/gm)) {
2154
+ const [, tag, rest] = m;
2155
+ if (!Engine.KNOWN.has(tag)) {
2156
+ warnings.push(`${tag} is not a directive this engine knows — it will be ignored.`);
2157
+ }
2158
+ else if (rest.trim() && !/^\s*[\w.\-+]+(\s+[\w.\-+]+)*\s*$/.test(rest)) {
2159
+ warnings.push(`${tag} has a note on the same line, so it does not parse and is being IGNORED. ` +
2160
+ `A directive must be alone on its line — put the note on the next one.`);
2161
+ }
2162
+ }
2173
2163
  if (parseParticleBloom(wgsl) && !wantsParticles && !wantsTrails) {
2174
2164
  warnings.push("// @bloom does nothing here. A field effect (background/foreground) composites after tone " +
2175
2165
  "mapping, past the bloom pyramid — the directive applies to particles and ribbons, which draw " +
@@ -2406,7 +2396,22 @@ export class Engine {
2406
2396
  epochScene: this.sceneClock,
2407
2397
  // Its OWN resolution, no longer the scene's: an effect that never asked
2408
2398
  // for full res is not promoted because a neighbour did.
2409
- fieldLayer: /^\s*\/\/\s*@fullres\s*$/m.test(wgsl) ? 0 : 1,
2399
+ // FULL RESOLUTION UNLESS TOLD OTHERWISE.
2400
+ //
2401
+ // It was the other way round, and the default was the bug. An author
2402
+ // who has never heard of the flag writes an effect with an edge in it
2403
+ // and gets a soft one — nothing fails, nothing warns, because nothing
2404
+ // was declared to fail. Three shipped effects DID declare it and were
2405
+ // half-res anyway on a parsing technicality, which is the same bug
2406
+ // wearing a different hat: the safe answer has to be the one you get
2407
+ // for saying nothing.
2408
+ //
2409
+ // The cost is real and is why the half layer stays: `@halfres` is worth
2410
+ // about 3.7x on a full-screen effect (Footprints, measured, 1.2ms
2411
+ // against 4.5ms). It is the right call for a soft additive glow, which
2412
+ // upsamples invisibly — and it is now a claim an author makes about
2413
+ // their own effect rather than a fate that befalls one.
2414
+ fieldLayer: /^\s*\/\/\s*@halfres\s*$/m.test(wgsl) ? 1 : 0,
2410
2415
  fieldPipeline,
2411
2416
  fieldClock,
2412
2417
  // Filled by rebuildFieldBindGroup below, which needs the instance to
@@ -3345,7 +3350,9 @@ export class Engine {
3345
3350
  return null;
3346
3351
  const view = this.camera.getViewMatrix().values;
3347
3352
  for (const inst of this.modelInstances.values()) {
3348
- if (!inst.model.visible || inst.isStage)
3353
+ // Neither a stage nor a plane is a performer, so neither is a subject an
3354
+ // effect can follow.
3355
+ if (!inst.model.visible || inst.isStage || inst.isPlane)
3349
3356
  continue;
3350
3357
  const model = inst.model;
3351
3358
  const matrices = model.getWorldMatrices();
@@ -5712,7 +5719,7 @@ export class Engine {
5712
5719
  // is first in insertion order and was seeding this clock with its own
5713
5720
  // permanent zero. In a scene with a stage, a camera VMD therefore sampled
5714
5721
  // frame 0 forever and the shot never moved.
5715
- if (inst.isStage)
5722
+ if (inst.isStage || inst.isPlane)
5716
5723
  continue;
5717
5724
  const p = inst.model.getAnimationProgress();
5718
5725
  if (p.playing || p.paused)
@@ -6054,7 +6061,7 @@ export class Engine {
6054
6061
  const reader = assetReader ?? createFetchAssetReader();
6055
6062
  const basePath = deriveBasePathFromPmxPath(pmxPath);
6056
6063
  model.setAssetContext(reader, basePath);
6057
- await this.setupModelInstance(key, model, basePath, reader, options?.stage ?? false);
6064
+ await this.setupModelInstance(key, model, basePath, reader, options?.stage ?? false, options?.plane ?? false, options?.dynamic ?? false);
6058
6065
  return key;
6059
6066
  }
6060
6067
  /**
@@ -6082,6 +6089,163 @@ export class Engine {
6082
6089
  this.setModelTransform(key, options.transform);
6083
6090
  return key;
6084
6091
  }
6092
+ /**
6093
+ * Put a picture in the scene as a flat card.
6094
+ *
6095
+ * The thing compositors arrange in a post tool's fake 3D space — Nuke calls
6096
+ * it a Card, After Effects a 3D layer, MMD 板ポリ — except the space here is
6097
+ * the real one. A card is occluded by anything in front of it, occludes what
6098
+ * is behind it, takes perspective when turned, and is caught by depth of
6099
+ * field like everything else, because it is ordinary geometry rather than a
6100
+ * layer composited afterwards.
6101
+ *
6102
+ * It is a MODEL, deliberately. Not a new kind of scene object with its own
6103
+ * list, its own persistence and its own selection: a card wants a position,
6104
+ * a rotation and a size, which is exactly what a model already has, and
6105
+ * everything built around models — the transform, the shadow settings, the
6106
+ * material editor, the asset bundle — works on it the day it exists. It is
6107
+ * not a STAGE, though: it skips the same machinery for the same reasons, and
6108
+ * leaves the floor alone. See ModelInstance.isPlane.
6109
+ *
6110
+ * @returns the model key, for setModelTransform and removeModel.
6111
+ */
6112
+ async addPlane(options) {
6113
+ const { image, name, width, height } = options;
6114
+ const hw = Math.max(width, 1e-4) / 2;
6115
+ const hh = Math.max(height, 1e-4) / 2;
6116
+ // A quad on the XY plane, facing +Z, centred on its own origin — so a
6117
+ // rotation turns it about its middle and a position places its centre,
6118
+ // which is what a handle in the viewport implies.
6119
+ //
6120
+ // V IS FLIPPED, and this is the whole of it: a picture's rows run downward
6121
+ // from its top-left, a UV runs upward from the bottom-left, and a card that
6122
+ // renders its image upside down looks like a bug in everything else.
6123
+ // prettier-ignore
6124
+ const vertexData = new Float32Array([
6125
+ // x y z nx ny nz u v
6126
+ -hw, -hh, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0,
6127
+ hw, -hh, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0,
6128
+ hw, hh, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0,
6129
+ -hw, hh, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
6130
+ ]);
6131
+ // Two triangles, counter-clockwise seen from +Z. A second pair wound the
6132
+ // other way is how "visible from behind" is done here, rather than a
6133
+ // per-material cull flag the rest of the engine has no concept of.
6134
+ const indices = options.doubleSided ? [0, 1, 2, 0, 2, 3, 0, 2, 1, 0, 3, 2] : [0, 1, 2, 0, 2, 3];
6135
+ const indexData = new Uint32Array(indices);
6136
+ // The texture table's one entry. The path is a key, not a location — the
6137
+ // reader below answers it from memory, so nothing is fetched and nothing is
6138
+ // written to disk.
6139
+ //
6140
+ // A PLAIN RELATIVE NAME under a plain directory, because the loader treats
6141
+ // this exactly as it treats a PMX's: it takes the model path's directory
6142
+ // and JOINS the texture entry onto it. A scheme-looking path went through
6143
+ // that as `plane://` + `plane://name` and matched nothing, so every card
6144
+ // came out with the untextured fallback. `plane/<name>` joins to
6145
+ // `plane/<name>` and stays unique per card, which the engine-wide texture
6146
+ // cache needs it to be.
6147
+ const texturePath = `plane/${name}`;
6148
+ const material = {
6149
+ name,
6150
+ diffuse: [1, 1, 1, 1],
6151
+ specular: [0, 0, 0],
6152
+ ambient: [0, 0, 0],
6153
+ shininess: 0,
6154
+ diffuseTextureIndex: 0,
6155
+ normalTextureIndex: -1,
6156
+ sphereTextureIndex: -1,
6157
+ sphereMode: 0,
6158
+ toonTextureIndex: -1,
6159
+ sharedToon: false,
6160
+ // 0 CARRIES TWO DECISIONS, both wanted, and both silent if changed.
6161
+ //
6162
+ // No inverted-hull outline (bit 0x10): a card is not a character, and a
6163
+ // black rim around a light leak is the opposite of what it is for.
6164
+ //
6165
+ // AND NO SHADOW (bit 0x04, which is what castsShadow reads). A card is
6166
+ // usually light or artwork rather than an object, and a rectangle of hard
6167
+ // shadow thrown across the stage by a gradient reads as the renderer
6168
+ // being broken. Set geometry that SHOULD cast one is the rarer case, and
6169
+ // it can say so.
6170
+ edgeFlag: 0,
6171
+ edgeColor: [0, 0, 0, 1],
6172
+ edgeSize: 0,
6173
+ vertexCount: indices.length,
6174
+ };
6175
+ // ONE BONE, because Model requires one — it throws on an empty skeleton,
6176
+ // every vertex has to be skinned to something, and a card has nothing to
6177
+ // articulate. It is never posed; the model transform is what moves a card.
6178
+ const skeleton = {
6179
+ bones: [{ name: "全ての親", parentIndex: -1, bindTranslation: [0, 0, 0], children: [] }],
6180
+ inverseBindMatrices: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
6181
+ };
6182
+ const vertexCount = 4;
6183
+ const joints = new Uint16Array(vertexCount * 4);
6184
+ const weights = new Uint8Array(vertexCount * 4);
6185
+ for (let i = 0; i < vertexCount; i++)
6186
+ weights[i * 4] = 255;
6187
+ const model = new Model(vertexData, indexData, [{ path: name, name }], [material], skeleton, { joints, weights }, { morphs: [] });
6188
+ // ITS OWN PATH, not addStage's. A plane and a stage skip the same machinery
6189
+ // and mean different things, and routing one through the other is how the
6190
+ // ground came to be suppressed by adding a picture.
6191
+ // The picture answers from memory, whatever it is asked for: a card has
6192
+ // exactly ONE texture, so there is nothing to disambiguate and no way for a
6193
+ // path to be wrong. Matching the string instead is what silently produced
6194
+ // untextured cards, because the loader composes that string itself.
6195
+ const reader = { readBinary: async () => image };
6196
+ const key = await this.addModel(model, texturePath, name, reader, { plane: true, dynamic: options.dynamic });
6197
+ // UNLIT, because a card is FOOTAGE and not a surface.
6198
+ //
6199
+ // Its pixels were finished somewhere else — a gradient painted in
6200
+ // Photoshop, a title, a rendered element — so its brightness is the artwork
6201
+ // rather than a response to anything. Shading it means the sun dimming one
6202
+ // side of a thing that has no sides, and the world colour tinting a picture
6203
+ // whose colour was the point. Left ungrouped it would take the neutral
6204
+ // Principled base, which is exactly that mistake.
6205
+ //
6206
+ // A group, not a hard-coded pipeline: a card used as SET geometry — a photo
6207
+ // of a wall, a poster standing in the room — genuinely does want the light,
6208
+ // and this is the same control every other material is changed through, so
6209
+ // that case is a graph swap rather than a feature request.
6210
+ await this.applyStyleGroups(key, [
6211
+ { id: "plane", label: "Plane", materials: [name], graph: UNLIT_GRAPH, alphaMode: "hashed" },
6212
+ ]);
6213
+ if (options.transform)
6214
+ this.setModelTransform(key, options.transform);
6215
+ // Kept so a moving card can push frames into it. The cache is keyed by the
6216
+ // texture's logical path, which is derived rather than stored anywhere the
6217
+ // caller can see — and deriving it twice is how the two would drift.
6218
+ const tex = this.textureCache.get(texturePath);
6219
+ if (tex)
6220
+ this.planeTextures.set(key, tex);
6221
+ return key;
6222
+ }
6223
+ /**
6224
+ * Replace what a card is showing, in place.
6225
+ *
6226
+ * For a moving card: a video element, a decoded frame, a canvas — anything
6227
+ * copyExternalImageToTexture accepts. Nothing is reallocated and no bind group
6228
+ * is rebuilt, so this is a per-frame call rather than a per-clip one; the
6229
+ * texture is written where it stands and the material keeps pointing at it.
6230
+ *
6231
+ * The frame must be the size the card was created at. A card is a fixed
6232
+ * rectangle of texels and resizing one mid-clip would mean rebuilding the
6233
+ * material behind it — so the caller allocates the card at its video's size
6234
+ * and this refuses anything else rather than stretching it silently.
6235
+ */
6236
+ setPlaneFrame(id, source, width, height) {
6237
+ const tex = this.planeTextures.get(id);
6238
+ if (!tex || !this.device)
6239
+ return false;
6240
+ if (tex.width !== width || tex.height !== height)
6241
+ return false;
6242
+ this.device.queue.copyExternalImageToTexture({ source }, { texture: tex }, [width, height]);
6243
+ // A moving card is allocated with one level precisely so this is never
6244
+ // reached: rebuilding a mip pyramid per frame is a pass per level per card.
6245
+ if (tex.mipLevelCount > 1)
6246
+ this.generateMipmaps(tex, tex.mipLevelCount);
6247
+ return true;
6248
+ }
6085
6249
  /** True while a stage is in the scene. Two things turn on it: the built-in
6086
6250
  * ground plane must not draw, and the far shadow cascade has nothing to
6087
6251
  * cover without one (see the cascade loop). */
@@ -6100,6 +6264,9 @@ export class Engine {
6100
6264
  const inst = this.modelInstances.get(name);
6101
6265
  if (!inst)
6102
6266
  return;
6267
+ // Before the texture cache below frees it: a stale entry here would hand a
6268
+ // destroyed texture to the next setPlaneFrame.
6269
+ this.planeTextures.delete(name);
6103
6270
  inst.model.stop();
6104
6271
  for (const path of inst.textureCacheKeys) {
6105
6272
  const tex = this.textureCache.get(path);
@@ -6352,10 +6519,10 @@ export class Engine {
6352
6519
  // A stage never solves IK — nothing drives its chains — and skips the pose
6353
6520
  // pass entirely while it is idle. Morph changes still come through, since
6354
6521
  // that is the one thing a stage's controls do move.
6355
- const stageIdle = inst.isStage && inst.model.isIdle();
6522
+ const stageIdle = (inst.isStage || inst.isPlane) && inst.model.isIdle();
6356
6523
  let verticesChanged = false;
6357
6524
  if (!stageIdle) {
6358
- verticesChanged = inst.model.update(deltaTime, inst.isStage ? false : this.ikEnabled);
6525
+ verticesChanged = inst.model.update(deltaTime, inst.isStage || inst.isPlane ? false : this.ikEnabled);
6359
6526
  inst.skinMatricesDirty = true;
6360
6527
  }
6361
6528
  animMs += performance.now() - tAnim;
@@ -7305,7 +7472,7 @@ export class Engine {
7305
7472
  });
7306
7473
  }
7307
7474
  }
7308
- async setupModelInstance(name, model, basePath, assetReader, isStage = false) {
7475
+ async setupModelInstance(name, model, basePath, assetReader, isStage = false, isPlane = false, dynamicTexture = false) {
7309
7476
  const vertices = model.getVertices();
7310
7477
  const skinning = model.getSkinning();
7311
7478
  const skeleton = model.getSkeleton();
@@ -7348,7 +7515,7 @@ export class Engine {
7348
7515
  // A stage never simulates, so its bodies are never built — constructing the
7349
7516
  // solver for the heaviest mesh in the scene and dropping it afterwards was
7350
7517
  // both wasted work and an invariant maintained in the wrong place.
7351
- const physics = !isStage && rbs.length > 0 ? new RezePhysics(rbs, model.getJoints()) : null;
7518
+ const physics = !isStage && !isPlane && rbs.length > 0 ? new RezePhysics(rbs, model.getJoints()) : null;
7352
7519
  // Which bones the simulation will overwrite, handed to the pose pipeline so
7353
7520
  // the append (付与) pass can consume the simulated result instead of the
7354
7521
  // animated one. Precomputed here, once, because the answer is topology —
@@ -7420,6 +7587,8 @@ export class Engine {
7420
7587
  pickPerInstanceBindGroup,
7421
7588
  pickDrawCalls: [],
7422
7589
  isStage,
7590
+ isPlane,
7591
+ dynamicTexture,
7423
7592
  // Seeded true: the bind pose has to reach the GPU once before any frame.
7424
7593
  skinMatricesDirty: true,
7425
7594
  hiddenMaterials: new Set(),
@@ -7841,7 +8010,21 @@ export class Engine {
7841
8010
  bounds[3] += grow;
7842
8011
  bounds[4] += grow;
7843
8012
  bounds[5] += grow;
7844
- const type = isTransparent ? "transparent" : "opaque";
8013
+ // A CARD IS ALWAYS OPAQUE-PHASE, whatever its alpha says.
8014
+ //
8015
+ // The scene pass runs opaque -> ground -> transparent, and the ground
8016
+ // writes depth at every opacity (effects locate the floor by it). Every
8017
+ // card qualifies as transparent — a cutout has translucent texels, and a
8018
+ // video card starts from a blank sheet that is nothing but — so cards
8019
+ // drew after the ground and an INVISIBLE floor rejected them. Turning the
8020
+ // ground down for the shadow catcher made pictures disappear into it.
8021
+ //
8022
+ // Not a workaround: alphaMode "hashed" is alpha-to-coverage, which is the
8023
+ // transparency technique built for this phase, and addPlane already sets
8024
+ // it. The cost is dithering on a large soft gradient, where MSAA has four
8025
+ // coverage levels to spend — a cutout edge, which is what a card usually
8026
+ // has, resolves exactly.
8027
+ const type = inst.isPlane ? "opaque" : isTransparent ? "transparent" : "opaque";
7845
8028
  inst.drawCalls.push({
7846
8029
  type,
7847
8030
  count: indexCount,
@@ -8134,7 +8317,13 @@ export class Engine {
8134
8317
  `so sheer fabric will not blend. The canvas 2D readback is what failed.`);
8135
8318
  }
8136
8319
  this.textureAlphaCache.set(cacheKey, alphaPlane);
8137
- const mipLevelCount = Math.floor(Math.log2(Math.max(width, height))) + 1;
8320
+ // NO MIPS FOR A MOVING CARD. The chain would have to be rebuilt on every
8321
+ // frame written into it — a full pyramid of render passes per video plane
8322
+ // per frame, which is most of what a moving card was costing. Level 0 is
8323
+ // the only level a card in frame reads anyway; the price is aliasing on one
8324
+ // shrunk far into the distance, which is the case a video card is least
8325
+ // often in.
8326
+ const mipLevelCount = inst.dynamicTexture ? 1 : Math.floor(Math.log2(Math.max(width, height))) + 1;
8138
8327
  const texture = this.device.createTexture({
8139
8328
  label: `texture: ${cacheKey}`,
8140
8329
  size: [width, height],
@@ -10337,6 +10526,66 @@ Engine.FILMIC_LUT_WIDTH = 256;
10337
10526
  // (vsync-to-vsync), recomputed at STATS_REFRESH_MS so the readout doesn't flicker.
10338
10527
  Engine.STATS_WINDOW = 120;
10339
10528
  Engine.STATS_REFRESH_MS = 500;
10529
+ /**
10530
+ * Install the scene's WGSL effect (shadertoy-style), rendered per-pixel in the
10531
+ * composite pass. ONE effect per scene, and the code says where it mounts by
10532
+ * which of these it defines — either, or both in one file:
10533
+ *
10534
+ * fn background(ray: vec3f, uv: vec2f, time: f32) -> vec4f
10535
+ * fn foreground(ray: vec3f, uv: vec2f, time: f32, depth: f32) -> vec4f
10536
+ *
10537
+ * `background` is a LAYER between the base background and the scene,
10538
+ * over-composited onto whichever base is active (solid color, 360 equirect, or
10539
+ * transparency) — its alpha lets the base show through, so a starfield is
10540
+ * stars over the user's background color. `foreground` composites over the
10541
+ * finished frame instead, which is where rain, snow, petals and fog live, and
10542
+ * is handed `depth`: the camera-space distance in metres of whatever the scene
10543
+ * drew at that pixel (the far plane where it drew nothing). Compare a
10544
+ * particle's own distance against it and the model occludes it; fog just reads
10545
+ * it, since fog's alpha IS a function of distance.
10546
+ *
10547
+ * `ray` is the pixel's normalized world-space view direction (LH, +Z forward —
10548
+ * what the skybox samples by), `uv` is 0..1 bottom-left origin, `time` is
10549
+ * seconds since apply, and `bgResolution()` gives the canvas size. Return sRGB
10550
+ * + alpha; alpha is the only "how much does this replace" control there is.
10551
+ * Declared `params` arrive as `params.<name>` (number → f32, Vec3 → vec3f),
10552
+ * shared by both mounts, and are later tweaked without recompiling via
10553
+ * setEffectParam.
10554
+ *
10555
+ * Both mounts are display-space: neither affects lighting, bloom or
10556
+ * tonemapping, and both are captured by offline export. A foreground makes the
10557
+ * scene pass STORE its depth buffer (it otherwise discards it into tile
10558
+ * memory) for as long as one is installed.
10559
+ *
10560
+ * Compiles off the hot path (async pipelines): on failure the previous effect
10561
+ * is KEPT and diagnostics are returned with line numbers relative to the
10562
+ * user's WGSL. Pass null to remove the effect.
10563
+ */
10564
+ /**
10565
+ * Every directive an effect may declare — used ONLY to tell an author that
10566
+ * the line they wrote is not doing what they think. See compileEffect.
10567
+ *
10568
+ * It has to be complete, including the ones this engine does not read itself:
10569
+ * `@dissolve` is parsed by the host, and warning about it would be worse than
10570
+ * the silence this replaced — a false alarm on a working line teaches authors
10571
+ * to ignore the channel.
10572
+ */
10573
+ Engine.KNOWN = new Set([
10574
+ "@anchor",
10575
+ "@layer",
10576
+ "@blend",
10577
+ "@bloom",
10578
+ "@lights",
10579
+ "@grid",
10580
+ "@particles",
10581
+ "@halfres",
10582
+ // Accepted and inert: full resolution is the default now, and an effect
10583
+ // that still says so is right about what it wants. Warning about it would
10584
+ // be telling authors off for the thing that used to be necessary.
10585
+ "@fullres",
10586
+ // The HOST's, not this engine's — see the note above.
10587
+ "@dissolve",
10588
+ ]);
10340
10589
  // ── GPU frustum cull ────────────────────────────────────────────────────────
10341
10590
  //
10342
10591
  // Sizes, once, so the arithmetic below is readable: a DrawMeta is 32 bytes
@@ -0,0 +1,3 @@
1
+ import type { ShaderGraph } from "../schema";
2
+ export declare const UNLIT_GRAPH: ShaderGraph;
3
+ //# sourceMappingURL=unlit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unlit.d.ts","sourceRoot":"","sources":["../../../src/graph/presets/unlit.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,eAAO,MAAM,WAAW,EAAE,WAmBzB,CAAA"}
@@ -0,0 +1,34 @@
1
+ // Unlit — the texture, at its own brightness, and nothing else.
2
+ //
3
+ // What FOOTAGE needs. A media plane carries pixels somebody already finished
4
+ // somewhere else: a gradient painted in Photoshop, a title card, a rendered
5
+ // element. Shading it is not a stylistic choice but a mistake — the scene's sun
6
+ // would dim one side of a card that has no side, and the world colour would
7
+ // tint artwork whose colour is the point. Blender's answer is the same one:
8
+ // wire the image straight into an Emission shader and let it out at the value
9
+ // it was authored at.
10
+ //
11
+ // Emission rather than Principled with roughness 1: emission is radiance, so it
12
+ // leaves the light loop entirely instead of being an unusually flat surface
13
+ // inside it. That is also what lets a card sit in front of a lamp without
14
+ // picking up its highlight.
15
+ export const UNLIT_GRAPH = {
16
+ version: 1,
17
+ name: "Unlit",
18
+ tags: ["unlit", "plane"],
19
+ nodes: [
20
+ { id: "tex", type: "texture" },
21
+ // The PMX material colour still multiplies in, so a plane can be tinted or
22
+ // faded through the same dial every other material uses rather than needing
23
+ // one of its own.
24
+ { id: "mat", type: "material_diffuse" },
25
+ { id: "base", type: "mix/multiply", inputs: { fac: 1.0 } },
26
+ { id: "emit", type: "emission", inputs: { strength: 1.0 } },
27
+ ],
28
+ links: [
29
+ { from: { node: "tex", socket: "color" }, to: { node: "base", socket: "a" } },
30
+ { from: { node: "mat", socket: "color" }, to: { node: "base", socket: "b" } },
31
+ { from: { node: "base", socket: "color" }, to: { node: "emit", socket: "color" } },
32
+ ],
33
+ output: { node: "emit", socket: "color" },
34
+ };
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export { BODY_GRAPH } from "./graph/presets/body";
18
18
  export { STOCKINGS_GRAPH } from "./graph/presets/stockings";
19
19
  export { EYE_GRAPH } from "./graph/presets/eye";
20
20
  export { FACE_GRAPH } from "./graph/presets/face";
21
+ export { UNLIT_GRAPH } from "./graph/presets/unlit";
21
22
  export { Model, MATERIAL_MORPH_MULTIPLY, MATERIAL_MORPH_ADD, type ClipEventInfo, type RootMotionProfile, type Morph, type Morphing, type BoneMorphOffset, type MaterialMorphOffset, type UvMorphOffset, } from "./model";
22
23
  export { Vec3, Quat, Mat4, easeInOut, type EulerOrder } from "./math";
23
24
  export type { AnimationClip, AnimationPlayOptions, AnimationProgress, BlendEntry, BoneKeyframe, IkKeyframe, MorphKeyframe, BoneInterpolation, ControlPoint, } from "./animation";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export { BODY_GRAPH } from "./graph/presets/body";
22
22
  export { STOCKINGS_GRAPH } from "./graph/presets/stockings";
23
23
  export { EYE_GRAPH } from "./graph/presets/eye";
24
24
  export { FACE_GRAPH } from "./graph/presets/face";
25
+ export { UNLIT_GRAPH } from "./graph/presets/unlit";
25
26
  export { Model, MATERIAL_MORPH_MULTIPLY, MATERIAL_MORPH_ADD, } from "./model";
26
27
  export { Vec3, Quat, Mat4, easeInOut } from "./math";
27
28
  export { LocomotionController, } from "./locomotion";
@@ -5,8 +5,17 @@ export declare const LYRICS_FLOATS: number;
5
5
  /** Bounds on the line atlas the host packs rasterised lines into. It is sized
6
6
  * to the track that arrives rather than allocated at the maximum: a scene with
7
7
  * no lyrics carries a 1×1 placeholder, and a song's atlas is as tall as its
8
- * own lines need. 8192 is the smallest texture dimension WebGPU guarantees. */
9
- export declare const LYRIC_ATLAS_MAX_W = 2048;
8
+ * own lines need. 8192 is the smallest texture dimension WebGPU guarantees.
9
+ *
10
+ * WIDTH IS WHAT DECIDES SHARPNESS on a long line. A lyric spans most of the
11
+ * frame — the shipped effect leaves 6% a side — so at 4K it is drawn across
12
+ * some 3400 pixels, and 2048 could only ever be stretched to cover them. That
13
+ * is the stair-stepped text on a 4K render, and no row height fixes it: the
14
+ * line is already as wide as the sheet. 1080p never showed it, because 1690
15
+ * pixels of line fit inside 2048. The atlas is r8unorm, one byte a texel, and
16
+ * only ever as wide as its own longest line — so this ceiling costs nothing
17
+ * until a song actually needs it. */
18
+ export declare const LYRIC_ATLAS_MAX_W = 4096;
10
19
  export declare const LYRIC_ATLAS_MAX_H = 8192;
11
20
  export type LyricLine = {
12
21
  /** Seconds on the scene clock. */
@@ -1 +1 @@
1
- {"version":3,"file":"lyrics-api.d.ts","sourceRoot":"","sources":["../../src/shaders/lyrics-api.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,eAAO,MAAM,aAAa,QAAgD,CAAA;AAE1E;;;gFAGgF;AAChF,eAAO,MAAM,iBAAiB,OAAO,CAAA;AACrC,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC,MAAM,MAAM,SAAS,GAAG;IACtB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAExD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,CA+BpD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,YAAY,CAAC,WAAW,CAAC,CAkB7F;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAsDhE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAgC5F"}
1
+ {"version":3,"file":"lyrics-api.d.ts","sourceRoot":"","sources":["../../src/shaders/lyrics-api.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,eAAe,MAAM,CAAA;AAClC,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,eAAO,MAAM,YAAY,IAAI,CAAA;AAC7B,eAAO,MAAM,aAAa,QAAgD,CAAA;AAE1E;;;;;;;;;;;;sCAYsC;AACtC,eAAO,MAAM,iBAAiB,OAAO,CAAA;AACrC,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC,MAAM,MAAM,SAAS,GAAG;IACtB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;AAExD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,CA+BpD;AAED,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,YAAY,CAAC,WAAW,CAAC,CAkB7F;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAsDhE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAgC5F"}
@@ -26,8 +26,17 @@ export const LYRICS_FLOATS = LYRIC_HEADER + LYRIC_LINES_MAX * LYRIC_STRIDE;
26
26
  /** Bounds on the line atlas the host packs rasterised lines into. It is sized
27
27
  * to the track that arrives rather than allocated at the maximum: a scene with
28
28
  * no lyrics carries a 1×1 placeholder, and a song's atlas is as tall as its
29
- * own lines need. 8192 is the smallest texture dimension WebGPU guarantees. */
30
- export const LYRIC_ATLAS_MAX_W = 2048;
29
+ * own lines need. 8192 is the smallest texture dimension WebGPU guarantees.
30
+ *
31
+ * WIDTH IS WHAT DECIDES SHARPNESS on a long line. A lyric spans most of the
32
+ * frame — the shipped effect leaves 6% a side — so at 4K it is drawn across
33
+ * some 3400 pixels, and 2048 could only ever be stretched to cover them. That
34
+ * is the stair-stepped text on a 4K render, and no row height fixes it: the
35
+ * line is already as wide as the sheet. 1080p never showed it, because 1690
36
+ * pixels of line fit inside 2048. The atlas is r8unorm, one byte a texel, and
37
+ * only ever as wide as its own longest line — so this ceiling costs nothing
38
+ * until a song actually needs it. */
39
+ export const LYRIC_ATLAS_MAX_W = 4096;
31
40
  export const LYRIC_ATLAS_MAX_H = 8192;
32
41
  /**
33
42
  * Parse an .lrc file: `[mm:ss.xx]` tags (several per line share the text),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.50.12",
3
+ "version": "0.51.0",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",