Give a full-frame field a subject the eye can track, and let the song choose
what it does "One pattern across the screen" scenes have nothing to watch. Warping the field around a focal point was the previous attempt and it did not fix that — measured on Voronoi it raised orientation variety while layout stayed at 0.004, which is another way of saying the cells changed and the frame still had no subject. So a subject is now a form that does something TO the field, and which thing is the song's decision rather than the scene's: shift, warp, punch, morph or overlay. The same form punching a hole, bending the pattern or running it at another rate are three different videos, and across six songs the bank picks overlay, morph, shift, morph, warp and punch — so it is a real axis rather than a constant with five names. subjectSDF, subjectMask, subjectEdge and subjectWarp are in the contract, so any field scene can take a subject without knowing where the focal points are. Moiré Grid is the pilot and implements all five impacts. 0.031 to 0.037 — up a fifth, and the layout block moved from essentially nothing to 0.018, which is the first time anything has moved layout on a full-frame field. That is the number that speaks to the complaint: the frame's energy is no longer spread evenly, so there is somewhere to look. It is still under the 0.04 bar, and I have not seen it. The score says there is now a subject; whether it is worth watching is the question the gallery cannot answer. One bug worth recording: the subject helpers were placed above the ink in the preamble, and subjectEdge draws with inkStroke. GLSL has no forward declarations, so the whole preamble failed to compile and every scene using it rendered pure black — which the gallery reported as a variety of exactly 0.000 across all six blocks rather than as an error. An all-zero row means a dead shader, not a boring scene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
10eeddcf6d
commit
c950f8f3b1
@ -132,6 +132,7 @@ export const IDENTITY_UNIFORMS = {
|
|||||||
u_focusN: 'float', // how many focal points, 0 = none
|
u_focusN: 'float', // how many focal points, 0 = none
|
||||||
u_focusR: 'float', // their reach, in scene units
|
u_focusR: 'float', // their reach, in scene units
|
||||||
u_focusPull: 'float', // + draws the field in, - opens a void
|
u_focusPull: 'float', // + draws the field in, - opens a void
|
||||||
|
u_impact: 'float', // index into Identity.IMPACTS
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FRAME_UNIFORMS = [
|
export const FRAME_UNIFORMS = [
|
||||||
@ -553,6 +554,80 @@ vec3 inkValue(vec3 col) {
|
|||||||
return floor(col * n + 0.5) / n;
|
return floor(col * n + 0.5) / n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The subject lives below the ink because subjectEdge draws with inkStroke,
|
||||||
|
// and GLSL has no forward declarations — placed above, the whole preamble
|
||||||
|
// failed to compile and every scene using it rendered black.
|
||||||
|
/**
|
||||||
|
* The song's protagonist, standing at a focal point, as a signed distance.
|
||||||
|
*
|
||||||
|
* The missing piece for full-frame fields. A pattern spread evenly over the
|
||||||
|
* screen has nothing to watch and nothing to track: there is no subject, so the
|
||||||
|
* eye has nowhere to rest and no way to tell one song's version from another's.
|
||||||
|
*
|
||||||
|
* Warping the field around a focus was the previous attempt and it did not fix
|
||||||
|
* this. Measured on Voronoi Shatter it raised orientation variety from 0.140 to
|
||||||
|
* 0.194 while layout stayed at 0.004 — the cells changed shape and the frame's
|
||||||
|
* energy stayed exactly as evenly spread as before, which is the same thing as
|
||||||
|
* saying it still had no subject. A subject has to occupy part of the frame and
|
||||||
|
* leave the rest alone.
|
||||||
|
*
|
||||||
|
* The index argument picks which focal point. Returns a large positive distance when the
|
||||||
|
* song asked for no focus, so a scene can add it unconditionally.
|
||||||
|
*/
|
||||||
|
float subjectSDF(vec2 p, float index, float size) {
|
||||||
|
if (u_focusN < 0.5) return 1e3;
|
||||||
|
float s = max(size * stageScale(), 1e-3);
|
||||||
|
return castMain((p - focusAt(index)) / s) * s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The nearest subject, for scenes that just want "is there one here". */
|
||||||
|
float subjectSDF(vec2 p, float size) {
|
||||||
|
float d = 1e3;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (float(i) >= u_focusN) break;
|
||||||
|
d = min(d, subjectSDF(p, float(i), size));
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 0..1 inside the subject, with a soft edge. */
|
||||||
|
float subjectMask(vec2 p, float size) {
|
||||||
|
float d = subjectSDF(p, size);
|
||||||
|
return smoothstep(0.012, -0.012, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The subject's rim, for a line the eye can follow. */
|
||||||
|
float subjectEdge(vec2 p, float size) {
|
||||||
|
return inkStroke(subjectSDF(p, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displace a coordinate around the subject: the field bends near the form and
|
||||||
|
* is untouched away from it.
|
||||||
|
*
|
||||||
|
* This is the WARP impact, and it is what a scene reaches for when it wants the
|
||||||
|
* pattern itself disturbed rather than replaced.
|
||||||
|
*/
|
||||||
|
vec2 subjectWarp(vec2 p, float size, float amount) {
|
||||||
|
if (u_focusN < 0.5 || amount <= 0.0) return p;
|
||||||
|
vec2 q = p;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (float(i) >= u_focusN) break;
|
||||||
|
vec2 c = focusAt(float(i));
|
||||||
|
vec2 d = p - c;
|
||||||
|
float r = length(d);
|
||||||
|
float s = max(size * stageScale(), 1e-3);
|
||||||
|
// Strongest at the form's edge, nothing at its centre or far away.
|
||||||
|
float w = smoothstep(s * 2.2, s * 0.9, r) * smoothstep(0.0, s * 0.6, r);
|
||||||
|
q += normalize(d + 1e-5) * w * amount * s * 0.9;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Which impact the song chose. 0 shift, 1 warp, 2 punch, 3 morph, 4 overlay. */
|
||||||
|
bool impactIs(float which) { return abs(u_impact - which) < 0.5; }
|
||||||
|
|
||||||
|
|
||||||
vec3 prev(vec2 uv) {
|
vec3 prev(vec2 uv) {
|
||||||
if (u_hasPrev == 0) return vec3(0.0);
|
if (u_hasPrev == 0) return vec3(0.0);
|
||||||
return texture2D(u_prev, uv).rgb;
|
return texture2D(u_prev, uv).rgb;
|
||||||
|
|||||||
@ -32,6 +32,21 @@ export const FILLS = ['flat', 'ramp', 'hatch', 'stipple', 'halftone', 'hollow'];
|
|||||||
/** Lattices, as the shader's `u_latKind` index. */
|
/** Lattices, as the shader's `u_latKind` index. */
|
||||||
export const LATTICES = ['grid', 'radial', 'spiral', 'scatter', 'strata'];
|
export const LATTICES = ['grid', 'radial', 'spiral', 'scatter', 'strata'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the song's subject IMPACTS a field, as the shader's `u_impact` index.
|
||||||
|
*
|
||||||
|
* A pattern spread evenly over the screen has nothing to watch: no subject, so
|
||||||
|
* nowhere for the eye to rest and no way to tell one song's version from
|
||||||
|
* another's. Warping the field around a focal point was the first attempt and it
|
||||||
|
* changed the cells without changing where the frame's energy was, which is the
|
||||||
|
* same thing as still having no subject.
|
||||||
|
*
|
||||||
|
* So the subject is a form that does something TO the field, and which thing is
|
||||||
|
* the song's decision — the same form punching a hole, bending the pattern, or
|
||||||
|
* running it at a different rate are three different videos.
|
||||||
|
*/
|
||||||
|
export const IMPACTS = ['shift', 'warp', 'punch', 'morph', 'overlay'];
|
||||||
|
|
||||||
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -161,6 +176,7 @@ export function generateIdentity(summary, rng, sections = 4) {
|
|||||||
focusRadius: rng.range(0.35, 1.1),
|
focusRadius: rng.range(0.35, 1.1),
|
||||||
// Positive draws the field in and densifies it; negative opens a void.
|
// Positive draws the field in and densifies it; negative opens a void.
|
||||||
focusPull: (rng.bool(0.65) ? 1 : -1) * rng.range(0.3, 1),
|
focusPull: (rng.bool(0.65) ? 1 : -1) * rng.range(0.3, 1),
|
||||||
|
impact: rng.pick(IMPACTS),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -179,7 +195,7 @@ export const NEUTRAL_IDENTITY_UNIFORMS = {
|
|||||||
u_inkHatchScale: 80, u_inkOutline: 0, u_inkPosterize: 0,
|
u_inkHatchScale: 80, u_inkOutline: 0, u_inkPosterize: 0,
|
||||||
u_latKind: 3, u_latJitter: 0.5, u_latSpread: 0.9,
|
u_latKind: 3, u_latJitter: 0.5, u_latSpread: 0.9,
|
||||||
u_latScaleSpread: 0.3, u_latScaleBias: 0, u_latScale: 0.35,
|
u_latScaleSpread: 0.3, u_latScaleBias: 0, u_latScale: 0.35,
|
||||||
u_focusN: 0, u_focusR: 0.6, u_focusPull: 0,
|
u_focusN: 0, u_focusR: 0.6, u_focusPull: 0, u_impact: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -231,6 +247,7 @@ export function identityUniforms(identity, shape = null) {
|
|||||||
u_focusN: identity.lattice.focusCount,
|
u_focusN: identity.lattice.focusCount,
|
||||||
u_focusR: identity.lattice.focusRadius,
|
u_focusR: identity.lattice.focusRadius,
|
||||||
u_focusPull: identity.lattice.focusPull,
|
u_focusPull: identity.lattice.focusPull,
|
||||||
|
u_impact: IMPACTS.indexOf(identity.lattice.impact),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +262,7 @@ export function describeIdentity(identity) {
|
|||||||
return `cast ${form(a)} + ${form(b)} · ink ${ink.fill}` +
|
return `cast ${form(a)} + ${form(b)} · ink ${ink.fill}` +
|
||||||
`${ink.outline ? '+outline' : ''}${ink.posterize ? `/${ink.posterize}-tone` : ''}` +
|
`${ink.outline ? '+outline' : ''}${ink.posterize ? `/${ink.posterize}-tone` : ''}` +
|
||||||
` w${ink.weight.toFixed(2)} · on ${identity.lattice.kind}` +
|
` w${ink.weight.toFixed(2)} · on ${identity.lattice.kind}` +
|
||||||
` · ${identity.lattice.focusCount} focus` +
|
` · ${identity.lattice.focusCount} focus/${identity.lattice.impact}` +
|
||||||
` at ${identity.lattice.elementScale < 0.2 ? 'tiny' :
|
` at ${identity.lattice.elementScale < 0.2 ? 'tiny' :
|
||||||
identity.lattice.elementScale > 0.6 ? 'huge' : 'mid'} scale`;
|
identity.lattice.elementScale > 0.6 ? 'huge' : 'mid'} scale`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,12 @@ export const moireGrid = {
|
|||||||
traits: ['camera', 'style'],
|
traits: ['camera', 'style'],
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
|
// The subject: the song's protagonist standing in the field, with the
|
||||||
|
// interference running at a different phase inside it. A window rather
|
||||||
|
// than a shape laid on top — the pattern is still the picture, it just
|
||||||
|
// has somewhere to be about, and the edge gives the eye a line to track.
|
||||||
|
subject: { type: 'float', range: [0, 1], default: 0.7, uniform: 'u_subject' },
|
||||||
|
subjSize: { type: 'float', range: [0.2, 0.75], default: 0.42, uniform: 'u_subjSize' },
|
||||||
density: { type: 'float', range: [6, 60], default: 22, uniform: 'u_density', bias: 'density' },
|
density: { type: 'float', range: [6, 60], default: 22, uniform: 'u_density', bias: 'density' },
|
||||||
offset: { type: 'float', range: [0.0, 0.5], default: 0.08, uniform: 'u_offset' },
|
offset: { type: 'float', range: [0.0, 0.5], default: 0.08, uniform: 'u_offset' },
|
||||||
rotate: { type: 'float', range: [0, 0.25], default: 0.04, uniform: 'u_rotate', bias: 'motion', rate: true },
|
rotate: { type: 'float', range: [0, 0.25], default: 0.04, uniform: 'u_rotate', bias: 'motion', rate: true },
|
||||||
@ -55,14 +61,47 @@ vec4 scene(vec2 uv, vec2 p) {
|
|||||||
float a = grid(rot(t * 6.28318530718) * q, u_density, weight);
|
float a = grid(rot(t * 6.28318530718) * q, u_density, weight);
|
||||||
float b = grid(rot(-t * 6.28318530718 + u_offset * 3.14159) * (q + u_offset), u_density, weight);
|
float b = grid(rot(-t * 6.28318530718 + u_offset * 3.14159) * (q + u_offset), u_density, weight);
|
||||||
|
|
||||||
|
// The subject does something TO the field, and which thing is the song's
|
||||||
|
// decision rather than this scene's — the same form punching a hole,
|
||||||
|
// bending the grid or running it at another rate are three different
|
||||||
|
// videos. See Identity.IMPACTS.
|
||||||
|
float d = subjectSDF(p, u_subjSize);
|
||||||
|
float inside = smoothstep(0.01, -0.01, d) * u_subject;
|
||||||
|
|
||||||
|
// WARP bends the pattern around the form instead of replacing it inside,
|
||||||
|
// so it is applied to the coordinate everything below is drawn from.
|
||||||
|
if (impactIs(1.0)) q = subjectWarp(q, u_subjSize, u_subject * 1.2);
|
||||||
|
|
||||||
|
vec2 qi = q + vec2(u_offset * 0.7, -u_offset * 0.4);
|
||||||
|
float ai = grid(rot(t * 6.28318530718 * 1.35) * qi, u_density * 1.18, weight);
|
||||||
|
float bi = grid(rot(-t * 6.28318530718 * 0.7 + u_offset * 3.14159) * (qi + u_offset), u_density * 0.86, weight);
|
||||||
|
|
||||||
// The interference term is the point: where both grids land, it peaks.
|
// The interference term is the point: where both grids land, it peaks.
|
||||||
float interference = a * b;
|
float interference = a * b;
|
||||||
float either = max(a, b);
|
float either = max(a, b);
|
||||||
|
|
||||||
|
if (impactIs(0.0)) { // shift: a different beat inside
|
||||||
|
interference = mix(interference, ai * bi, inside);
|
||||||
|
either = mix(either, max(ai, bi), inside);
|
||||||
|
} else if (impactIs(2.0)) { // punch: the field stops at the form
|
||||||
|
interference *= 1.0 - inside;
|
||||||
|
either *= 1.0 - inside;
|
||||||
|
} else if (impactIs(3.0)) { // morph: one grid survives inside
|
||||||
|
interference = mix(interference, a * 0.35, inside);
|
||||||
|
either = mix(either, a, inside);
|
||||||
|
} else if (impactIs(4.0)) { // overlay: the form reads as solid
|
||||||
|
either = max(either, inside);
|
||||||
|
interference = max(interference, inside * 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
vec3 col = pal(0) * 0.05;
|
vec3 col = pal(0) * 0.05;
|
||||||
col += pal(1) * either * 0.35;
|
col += pal(1) * either * 0.35;
|
||||||
col += pal(2) * interference * (0.8 + 0.35);
|
col += pal(2) * interference * (0.8 + 0.35);
|
||||||
|
|
||||||
|
// The rim, so the form has an edge to follow rather than only a change of
|
||||||
|
// texture. Drawn in the song's hand like everything else.
|
||||||
|
col = mix(col, pal(3), inkStroke(d) * u_subject);
|
||||||
|
|
||||||
col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.3);
|
col *= 0.6 + 0.4 * exp(-dot(p, p) * 0.3);
|
||||||
return vec4(inkValue(col), 1.0);
|
return vec4(inkValue(col), 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ const CONTRACT_UNIFORMS = new Set([
|
|||||||
'u_inkHatchScale', 'u_inkOutline', 'u_inkPosterize',
|
'u_inkHatchScale', 'u_inkOutline', 'u_inkPosterize',
|
||||||
'u_latKind', 'u_latJitter', 'u_latSpread',
|
'u_latKind', 'u_latJitter', 'u_latSpread',
|
||||||
'u_latScaleSpread', 'u_latScaleBias', 'u_latScale',
|
'u_latScaleSpread', 'u_latScaleBias', 'u_latScale',
|
||||||
'u_focusN', 'u_focusR', 'u_focusPull',
|
'u_focusN', 'u_focusR', 'u_focusPull', 'u_impact',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user