first commit

This commit is contained in:
2026-10-06 12:16:46 +09:00
commit 619d980098
415 changed files with 22283 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const shader = await readFile(new URL('../src/shaders/mosaic.frag', import.meta.url), 'utf8');
assert.match(shader, /curvedSlope=\.30\*cellDimensions\/max\(abs\(opticalTravel\),\.6\)\*signedCurvatureScale\(\)\*sin\(6\.2831853\*local\)/);
assert.match(shader, /smoothFacet=\.003\*vec2\(sin\(6\.2831853\*paneUv\.x\),sin\(6\.2831853\*paneUv\.y\)\)/);
assert.match(shader, /float opticalTravel=-max\(abs\(uSourceZ-gh\.z\),\.6\)/);
assert.match(shader, /if\(style<=\.5\)\{\s*vec2 smoothFacet/);
assert.match(shader, /vec3 referenceI=normalize\(gh-vec3\(0\.,0\.,uContentCameraPosition\.z\)\),referenceRay=bend\(referenceI,nCurved\)/);
assert.match(shader, /incidenceDelta\/sqrt\(incidenceDelta\*incidenceDelta\+vec2\(\.025\*\.025\)\)/);
assert.match(shader, /cellEnvelope\*cellDimensions\*signedCurvatureScale\(\)\*\.18/);
assert.match(shader, /edgeTaper=style<=\.5\?1\.25:\.75/);
assert.match(shader, /float normalBlend=\.16/);
assert.match(shader, /vec3 sceneHit=straightHit\+vec3\(worldDelta\*strength,0\.\);vec2 finalSceneUv=projectUv\(sceneHit\)/);
assert.match(shader, /color=sampleScene\(finalSceneUv\)/);
const TAU = Math.PI * 2;
const cameraZ = 3;
const glassZ = 0.6;
const strengthSetting = 0.9;
const fovScale = Math.tan(20 * Math.PI / 180);
const add = (a, b) => a.map((v, i) => v + b[i]);
const sub = (a, b) => a.map((v, i) => v - b[i]);
const mul = (a, s) => a.map(v => v * s);
const dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
const norm = a => mul(a, 1 / Math.hypot(...a));
const safeZ = z => Math.abs(z) < 1e-4 ? (z < 0 ? -1e-4 : 1e-4) : z;
const smoothstep = (a, b, x) => {
const t = Math.max(0, Math.min(1, (x - a) / (b - a)));
return t * t * (3 - 2 * t);
};
const softLimit = (value, limit) => value * limit / (limit + Math.abs(value));
const surfaceCurvature = curvature => {
const clamped = Math.max(-1, Math.min(1, curvature));
const excess = Math.max(clamped - 0.65, 0);
return clamped - (0.25 / 0.35) * excess * excess;
};
const curvatureScale = curvature => Math.max(-1.25, Math.min(1.25, surfaceCurvature(curvature) / 0.65));
function refract(i, n) {
const eta = 0.75;
const d = dot(n, i);
const k = 1 - eta * eta * (1 - d * d);
if (k < 0) return i;
return sub(mul(i, eta), mul(n, eta * d + Math.sqrt(k)));
}
function mapToSource(uv, { gridSize, curvature, pane, cameraXY, sourceZ }) {
const [width, height] = pane;
const grid = [gridSize, Math.max(1, gridSize * height / width)];
const cellSize = [1 / grid[0], 1 / grid[1]];
const cellDimensions = [width / grid[0], height / grid[1]];
const cellWorld = Math.min(...cellDimensions);
const gx = uv[0] * grid[0], gy = uv[1] * grid[1];
const local = [gx - Math.floor(gx) - 0.5, gy - Math.floor(gy) - 0.5];
const gh = [(uv[0] - 0.5) * width, (uv[1] - 0.5) * height, glassZ];
const camera = [cameraXY[0], cameraXY[1], cameraZ];
const i = norm(sub(gh, camera));
const opticalTravel = -Math.max(Math.abs(sourceZ - gh[2]), 0.6);
const signedScale = curvatureScale(curvature);
const curvedSlope = [0, 1].map(axis =>
0.30 * cellDimensions[axis] / Math.max(Math.abs(opticalTravel), 0.6) * signedScale * Math.sin(TAU * local[axis])
);
const smoothFacet = [0.003 * Math.sin(TAU * uv[0]), 0.003 * Math.sin(TAU * uv[1])];
const nCurved = norm([curvedSlope[0] + smoothFacet[0], curvedSlope[1] + smoothFacet[1], 1]);
const curvedRay = refract(i, nCurved);
const rawDelta = [0, 1].map(axis =>
(curvedRay[axis] / safeZ(curvedRay[2]) - i[axis] / safeZ(i[2])) * opticalTravel
);
const worldDelta = rawDelta.map(value => softLimit(value, cellWorld * 0.55));
const referenceI = norm([gh[0], gh[1], gh[2] - cameraZ]);
const referenceRay = refract(referenceI, nCurved);
const cellEnvelope = local.map(value => 0.5 + 0.5 * Math.cos(TAU * value));
const incidenceDelta = [0, 1].map(axis => curvedRay[axis] / -curvedRay[2] - referenceRay[axis] / -referenceRay[2]);
const boundedIncidence = incidenceDelta.map(value => value / Math.sqrt(value * value + 0.025 ** 2));
for (let axis = 0; axis < 2; axis++) {
worldDelta[axis] += boundedIncidence[axis] * cellEnvelope[axis] * cellDimensions[axis] * signedScale * 0.18;
}
const straightT = (sourceZ - gh[2]) / safeZ(i[2]);
const straightHit = add(gh, mul(i, straightT));
const edgeDistance = Math.min(uv[0], uv[1], 1 - uv[0], 1 - uv[1]);
const edgeWeight = smoothstep(0, Math.max(...cellSize) * 1.25, edgeDistance);
const sceneHit = add(straightHit, [worldDelta[0] * strengthSetting * edgeWeight, worldDelta[1] * strengthSetting * edgeWeight, 0]);
const depth = cameraZ - sceneHit[2];
return [
0.5 + 0.5 * (sceneHit[0] - cameraXY[0]) / (depth * fovScale),
0.5 + 0.5 * (sceneHit[1] - cameraXY[1]) / (depth * fovScale)
];
}
const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
const grids = [6, 10, 96];
const curvatures = [-1, 0, 0.65, 1];
const panes = [[1.2, 0.8], [1.28, 1.6]];
const cameras = [[0, 0], [0.3, -0.3], [-0.6, 0.6], [-0.9, 0.9], [0.9, -0.9]];
const sourcePlanes = [0.16, 0.56];
const phases = [0.03, 0.25, 0.49, 0.5, 0.75, 0.97];
const interiorPhases = [-0.4, -0.25, 0, 0.25, 0.4];
let pointsChecked = 0;
let seamsChecked = 0;
for (const gridSize of grids) for (const curvature of curvatures) for (const pane of panes) {
for (const cameraXY of cameras) for (const sourceZ of sourcePlanes) {
const config = { gridSize, curvature, pane, cameraXY, sourceZ };
const points = [];
for (let line = 0; line <= gridSize; line++) {
const seam = line / gridSize;
for (const phase of phases) {
points.push([seam, phase], [phase, seam]);
}
for (let cell = 1; cell < gridSize; cell += Math.max(1, Math.floor(gridSize / 4))) {
for (const local of interiorPhases) {
points.push([(cell + 0.5 + local) / gridSize, (cell + 0.5) / gridSize]);
points.push([(cell + 0.5) / gridSize, (cell + 0.5 + local) / gridSize]);
}
}
}
points.push([0.001, 0.001], [0.999, 0.001], [0.001, 0.999], [0.999, 0.999]);
const h = Math.min(1e-6, 1e-4 / gridSize);
for (const [u0, v0] of points) {
const u = Math.max(h * 2, Math.min(1 - h * 2, u0));
const v = Math.max(h * 2, Math.min(1 - h * 2, v0));
const left = mapToSource([u - h, v], config), right = mapToSource([u + h, v], config);
const down = mapToSource([u, v - h], config), up = mapToSource([u, v + h], config);
const dx = [(right[0] - left[0]) / (2 * h), (right[1] - left[1]) / (2 * h)];
const dy = [(up[0] - down[0]) / (2 * h), (up[1] - down[1]) / (2 * h)];
const determinant = dx[0] * dy[1] - dy[0] * dx[1];
assert.ok(dx[0] > 0 && dy[1] > 0 && determinant > 0,
`folded projected UV at grid=${gridSize}, curvature=${curvature}, pane=${pane}, camera=${cameraXY}, sourceZ=${sourceZ}, uv=${u},${v}; jac=[${dx},${dy}]`);
pointsChecked++;
}
const epsilon = 1e-4 / gridSize;
for (let line = 1; line < gridSize; line++) {
const seam = line / gridSize;
for (const phase of [0.07, 0.31, 0.5, 0.69, 0.93]) {
for (const [axis, other] of [[0, phase], [1, phase]]) {
const a = axis === 0 ? [seam - epsilon, other] : [other, seam - epsilon];
const b = axis === 0 ? [seam + epsilon, other] : [other, seam + epsilon];
const aHalf = axis === 0 ? [seam - epsilon / 2, other] : [other, seam - epsilon / 2];
const bHalf = axis === 0 ? [seam + epsilon / 2, other] : [other, seam + epsilon / 2];
const fullStep = distance(mapToSource(a, config), mapToSource(b, config));
const halfStep = distance(mapToSource(aHalf, config), mapToSource(bHalf, config));
assert.ok(fullStep > 0 && Math.abs(fullStep - 2 * halfStep) < fullStep * 0.03,
`seam does not converge continuously at grid=${gridSize}, curvature=${curvature}, uv=${seam},${other}`);
seamsChecked++;
}
}
}
}
}
function displacementAt(uv, config) {
const { gridSize, pane, cameraXY, sourceZ } = config;
const [width, height] = pane;
const grid = [gridSize, Math.max(1, gridSize * height / width)];
const gh = [(uv[0] - 0.5) * width, (uv[1] - 0.5) * height, glassZ];
const i = norm(sub(gh, [cameraXY[0], cameraXY[1], cameraZ]));
const cell = [Math.floor(uv[0] * grid[0]), Math.floor(uv[1] * grid[1])];
const local = [uv[0] * grid[0] - cell[0] - 0.5, uv[1] * grid[1] - cell[1] - 0.5];
const cellDimensions = [width / grid[0], height / grid[1]];
const travel = -Math.max(Math.abs(sourceZ - glassZ), 0.6);
const scale = curvatureScale(config.curvature);
const slope = local.map((v, axis) => 0.30 * cellDimensions[axis] / Math.max(Math.abs(travel), 0.6) * scale * Math.sin(TAU * v));
const smoothFacet = [0.003 * Math.sin(TAU * uv[0]), 0.003 * Math.sin(TAU * uv[1])];
const n = norm([slope[0] + smoothFacet[0], slope[1] + smoothFacet[1], 1]);
const ray = refract(i, n);
const reference = refract(norm([gh[0], gh[1], gh[2] - cameraZ]), n);
const envelope = local.map(v => 0.5 + 0.5 * Math.cos(TAU * v));
return [0, 1].map(axis => {
const a = ray[axis] / -ray[2] - reference[axis] / -reference[2];
return a / Math.sqrt(a * a + 0.025 ** 2) * envelope[axis] * cellDimensions[axis] * scale * 0.18;
});
}
for (const curvature of [-1, -0.65, 0.65, 1]) {
const centered = { gridSize: 10, curvature, pane: [1.2, 0.8], cameraXY: [0, 0], sourceZ: 0.16 };
const cameraView = { ...centered, cameraXY: [0.3, -0.3] };
const gridY = Math.max(1, centered.gridSize * centered.pane[1] / centered.pane[0]);
const cellCenter = [(4 + 0.5) / centered.gridSize, (Math.floor(gridY / 2) + 0.5) / gridY];
const xEdges = [4 / centered.gridSize, 5 / centered.gridSize];
const yIndex = Math.floor(gridY / 2), yEdges = [yIndex / gridY, (yIndex + 1) / gridY];
const atCellCenter = displacementAt(cellCenter, centered);
assert.ok(Math.hypot(...atCellCenter) < 1e-10, `center view should add no local displacement at curvature=${curvature}`);
const edgePoints = [
[[xEdges[0], cellCenter[1]], 0], [[xEdges[1], cellCenter[1]], 0],
[[cellCenter[0], yEdges[0]], 1], [[cellCenter[0], yEdges[1]], 1]
];
for (const [uv, fixedAxis] of edgePoints) {
assert.ok(Math.abs(displacementAt(uv, cameraView)[fixedAxis]) < 1e-8, `cell edge axis should remain fixed at curvature=${curvature}, uv=${uv}`);
}
const samples = [
[cellCenter[0] - 0.02, cellCenter[1]], cellCenter, [cellCenter[0] + 0.02, cellCenter[1]],
[cellCenter[0], cellCenter[1] - 0.02], [cellCenter[0], cellCenter[1] + 0.02]
];
const offsets = samples.map(uv => displacementAt(uv, cameraView));
const mean = offsets.reduce((sum, value) => add(sum, value), [0, 0]).map(value => value / offsets.length);
const variance = offsets.reduce((sum, value) => sum + dot(sub(value, mean), sub(value, mean)), 0) / offsets.length;
assert.ok(variance > 1e-8, `camera incidence should create varying within-cell displacement at curvature=${curvature}`);
}
console.log(`Mosaic projected-UV continuity passed: ${pointsChecked} Jacobians, ${seamsChecked} seam convergence checks, center/edge anchors, and view-dependent within-cell displacement.`);