The song brings a body, not only an outline
A silhouette is the same picture from every angle, so a scene that turns one is
showing you the same shape rotated. That is the ceiling the cast has been under:
it can be notched and hollowed and it still cannot be walked around.
So the identity generates an ASSEMBLY — two to six parts, each a primitive with
an offset, a scale, a rotation and a boolean op, all under a symmetry. The
symmetry is the load-bearing half: parts unioned at random positions read as
debris, the same parts folded read as designed, and only a designed object is
worth calling a protagonist. Prism parts are the existing 2D profile extruded,
so the solid and the silhouette stay one character rather than two generators
running side by side.
It travels as data like every other artifact: three vec4 rows per part in
u_formPart, plus the scalars. Scenes declare `consumes: ['form']` and get
castSDF3, castMarch, castSolid, castChorusSolid and castLit; with no identity
they fall back to the flat profile extruded, so the helpers are safe to call
unconditionally. The chorus is the same rows with fewer parts and its own
proportions — a relative, not a second generator, and no extra uniforms.
Three scenes carry it. Effigy is new and holds the object still while it turns.
Floating Geometry and Swarm were already loops of stamps and are now loops of
bodies; Swarm is what the chorus solid exists for. That is 34.8% of videos
containing a 3D cast, against 11.9% when only Effigy had it.
Measured, the outline does change rather than merely spin: over one turn the lit
area of Effigy's subject varies 14-113% against Soloist's 3-51% for the same
rotation. Whether that reaches the variety blocks is not yet measured, and
HOWTO-variety says so rather than claiming the win.
Four costs, each found by measuring rather than by reading:
* every pixel evaluated every instance's field — Swarm at 59ms/frame against a
60ms ceiling. Bounding-sphere reject first, now 12.3ms.
* instances overlap several deep at the top of the size range, and marching
all of them made Floating Geometry's own gate run for minutes. First-wins
instead of last-wins, which was arbitrary either way.
* a normal inside the march loop multiplies four copies of the SDF by the step
count, because GLSL unrolls a fixed bound. Hoisted out.
* the helpers in the shared preamble made all 68 scenes compile what 3 of them
call. FORM_PREAMBLE is appended per scene instead.
The lint's backtick check was green through two of my own breakages: quoting a
name in a doc comment adds backticks in PAIRS, so parity survives and the
pair-scanner just re-partitions the file. It now finds where a shader literal
opens and requires the next backtick to be a real terminator — which
immediately found a second stray pair.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,9 @@ const CONTRACT_UNIFORMS = new Set([
|
||||
'u_latKind', 'u_latJitter', 'u_latSpread',
|
||||
'u_latScaleSpread', 'u_latScaleBias', 'u_latScale',
|
||||
'u_focusN', 'u_focusR', 'u_focusPull', 'u_impact',
|
||||
'u_formCount', 'u_formSym', 'u_formSymN', 'u_formBlend', 'u_formDepth',
|
||||
'u_formPart', 'u_formChorusN', 'u_formChorusSym', 'u_formChorusSymN',
|
||||
'u_formChorusFlat', 'u_formChorusThin',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -123,55 +126,112 @@ const CONTRACT_UNIFORMS = new Set([
|
||||
// `consumes` is a comment, and the migration becomes unverifiable the moment it
|
||||
// is more than a handful of files.
|
||||
const ARTIFACT_EVIDENCE = {
|
||||
// The solid. Distinct from `cast` rather than a superset of it: these names
|
||||
// deliberately do not match the cast pattern below, so a scene that marches
|
||||
// the object is not also made to declare the silhouette it never stamps.
|
||||
form: /\b(castSDF3|castChorus3|castNormal3|castChorusNormal3|castMarch|castSolid|castChorusSolid|castLit)\s*\(/,
|
||||
cast: /\bcast(Main|Chorus|SDF|Form)\s*\(/,
|
||||
ink: /\bink(Mask|Value|Pattern|Stroke)\s*\(/,
|
||||
staging: /\b(stageNode|stageScale)\s*\(/,
|
||||
};
|
||||
|
||||
const TRAIT_EVIDENCE = {
|
||||
shape: /\b(sig(Shape|Form)|cast(Main|Chorus|SDF|Form))\s*\(/,
|
||||
// Marching the solid is the fullest expression of `shape` there is: the
|
||||
// prism parts ARE the signature profile, given a body.
|
||||
shape: /\b(sig(Shape|Form)|cast(Main|Chorus|SDF|Form|SDF3|Chorus3|March|Solid|ChorusSolid))\s*\(/,
|
||||
camera: /\bsigCamera\s*\(/,
|
||||
space: /\b(sigHorizonY|sigAir)\s*\(|\bu_sig(Horizon|Depth|Wash)\b/,
|
||||
style: /\b(sigEdge|sigGrain|sigFolded|ink(Mask|Stroke|Value|Pattern))\s*\(|\bu_sig(Line|Soft|Texture|Fold)\b/,
|
||||
};
|
||||
|
||||
// Every shader in this project lives inside a JS template literal, so a
|
||||
// backtick anywhere in one silently closes it. Three times now that has cost a
|
||||
// debugging round: twice in the preamble, where it produced a check page that
|
||||
// hung on "starting…" with an empty console, and once in a scene, where at
|
||||
// least the module failed to parse loudly. A GLSL comment is the natural place
|
||||
// to reach for backticks when quoting a param name, which is exactly why this
|
||||
// keeps happening.
|
||||
// backtick anywhere in one silently closes it. Five times now that has cost a
|
||||
// debugging round: in the preamble, where it produces a check page that hangs
|
||||
// on "starting…" with an empty console, and in scenes, where at least the
|
||||
// module fails to parse loudly. A GLSL comment is the natural place to reach
|
||||
// for backticks when quoting a param name, which is exactly why this keeps
|
||||
// happening.
|
||||
//
|
||||
// HOW THIS IS CHECKED, and why the obvious way does not work. The first version
|
||||
// counted backticks for parity and scanned each `...` pair for stray ones. Both
|
||||
// tests pass when the mistake comes in a PAIR — quoting `prop` in a doc comment
|
||||
// adds two, parity survives, and the pair-scanner simply re-partitions the file
|
||||
// into different "literals" and finds nothing inside them. That version was in
|
||||
// place, green, while the contract was broken.
|
||||
//
|
||||
// So the check is anchored instead: find where a shader literal OPENS, then
|
||||
// require that the next backtick is a real terminator — one followed by the
|
||||
// comma, semicolon or brace that closes the declaration. A backtick anywhere in
|
||||
// between is the bug, whatever the file's parity says.
|
||||
//
|
||||
// Checked across the contract AND every scene, since the scene case is the one
|
||||
// a mechanical pass over sixty files will keep reintroducing.
|
||||
/**
|
||||
* Index of the backtick that closes the template literal starting at `from`, or
|
||||
* -1. Interpolations are skipped wholesale, nested templates and all — the
|
||||
* preamble builds its uniform block with `${LIST.map((u) => \`…\`)}`, and those
|
||||
* inner backticks are legal.
|
||||
*/
|
||||
function endOfLiteral(src, from) {
|
||||
let i = from;
|
||||
let depth = 0;
|
||||
while (i < src.length) {
|
||||
const c = src[i];
|
||||
if (c === '\\') { i += 2; continue; }
|
||||
if (depth === 0 && c === '`') return i;
|
||||
if (c === '$' && src[i + 1] === '{') { depth++; i += 2; continue; }
|
||||
if (depth > 0) {
|
||||
if (c === '{') depth++;
|
||||
else if (c === '}') depth--;
|
||||
else if (c === '`') {
|
||||
const inner = endOfLiteral(src, i + 1);
|
||||
if (inner < 0) return -1;
|
||||
i = inner;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
console.log('\nshader literals');
|
||||
{
|
||||
const targets = [join(SRC, 'engine/shader-contract.js'), ...walk(join(SRC, 'scenes'))];
|
||||
let clean = 0;
|
||||
let literals = 0;
|
||||
for (const file of targets) {
|
||||
const src = readFileSync(file, 'utf8');
|
||||
const rel = relative(SRC, file).replace(/\\/g, '/');
|
||||
// Every template literal in the file, then the lines inside them that
|
||||
// carry a backtick without being an interpolation.
|
||||
const stray = [];
|
||||
const rx = /`([\s\S]*?)`/g;
|
||||
const problems = [];
|
||||
|
||||
// Where a shader string is declared: `shader: \`` or `const X = \``.
|
||||
const opens = /(?:shader\s*:|[A-Za-z_$][\w$]*\s*=)\s*`/g;
|
||||
let m;
|
||||
while ((m = rx.exec(src)) !== null) {
|
||||
if (!/\bvec4 scene|precision highp|void main/.test(m[1])) continue;
|
||||
for (const line of m[1].split('\n')) {
|
||||
if (line.includes('`') && !line.includes('${')) stray.push(line);
|
||||
while ((m = opens.exec(src)) !== null) {
|
||||
const from = m.index + m[0].length;
|
||||
const end = endOfLiteral(src, from);
|
||||
if (end < 0) { problems.push('unterminated template literal'); break; }
|
||||
|
||||
const body = src.slice(from, end);
|
||||
// Only shader strings are governed; an ordinary template literal is
|
||||
// free to contain whatever it likes.
|
||||
if (!/\bvec4 scene|precision highp|void main|gl_Position/.test(body)) continue;
|
||||
literals++;
|
||||
|
||||
const after = src.slice(end + 1, end + 4);
|
||||
if (!/^\s*[,;)\]}]/.test(after)) {
|
||||
const line = src.slice(0, end).split('\n').pop();
|
||||
problems.push(`closed early at: ${line.trim()}`);
|
||||
}
|
||||
opens.lastIndex = end + 1;
|
||||
}
|
||||
// A shader body that swallowed a closing backtick shows up as an
|
||||
// unbalanced count across the file.
|
||||
const ticks = (src.match(/`/g) || []).length;
|
||||
if (stray.length || ticks % 2 !== 0) {
|
||||
fail(`${rel}: backtick inside a shader literal — it closes the string` +
|
||||
(stray.length ? `:\n${stray.map((l) => ` ${l.trim()}`).join('\n')}` : ''));
|
||||
|
||||
if (problems.length) {
|
||||
fail(`${rel}: backtick inside a shader literal — it closes the string:\n` +
|
||||
problems.map((p) => ` ${p}`).join('\n'));
|
||||
} else clean++;
|
||||
}
|
||||
if (clean === targets.length) ok(`${clean} shader literals balanced, no stray backticks`);
|
||||
if (clean === targets.length) ok(`${literals} shader literals close where they should`);
|
||||
}
|
||||
|
||||
console.log('\nscene schema lint');
|
||||
|
||||
Reference in New Issue
Block a user