Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)

This commit is contained in:
WpyQwq
2026-09-19 13:04:56 +08:00
commit 3f422218a5
1098 changed files with 39410 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { decryptionFrame, DecryptionController } from "../src/decryption.ts";
import { CardAppearance } from "../src/appearance.ts";
import { frostedTransmissionLod, FROSTED_ROUGHNESS, CLEAR_ROUGHNESS } from "../src/glass-reveal.ts";
// Frost occupies the same fraction of a cover across viewport sizes. Clearing
// must never briefly increase its blur, and both original endpoints survive.
for (const scale of [0.5, 1, 2]) {
const pixels = 400 * scale, width = 1920 * scale;
assert.ok(Math.abs(2 ** frostedTransmissionLod(pixels, width, FROSTED_ROUGHNESS) / pixels - 0.016) < 1e-10);
let prior = -Infinity;
for (let i = 0; i <= 100; i++) {
const roughness = CLEAR_ROUGHNESS + (FROSTED_ROUGHNESS - CLEAR_ROUGHNESS) * i / 100;
const lod = frostedTransmissionLod(pixels, width, roughness);
assert.ok(lod >= prior - 1e-10, "Clearing monotonically reduces blur");
prior = lod;
assert.equal(frostedTransmissionLod(pixels, width, roughness, 0), Math.log2(width) * roughness * (1.46 * 2 - 2));
}
assert.ok(Math.abs(frostedTransmissionLod(pixels, width, CLEAR_ROUGHNESS) - Math.log2(width) * CLEAR_ROUGHNESS * (1.46 * 2 - 2)) < 1e-10);
}
const length = (frame) => frame.intervals.reduce((n, [a, b]) => n + b - a, 0);
let previous = 0;
for (let t = 34.24; t < 36.04; t += 0.001) {
const f = decryptionFrame(t),
current = length(f);
assert.ok(current >= previous - 1e-10 && current <= 1);
if (f.intervals.length)
assert.ok(Math.abs(f.intervals[0][1] + f.intervals[1][0] - 1) < 1e-10);
previous = current;
}
assert.deepEqual(decryptionFrame(36.04).intervals, [[0, 1]]);
assert.equal(length(decryptionFrame(37.71)), 1);
previous = 1;
for (let t = 37.72; t < 38.84; t += 0.001) {
const f = decryptionFrame(t),
current = length(f);
assert.ok(current <= previous + 1e-10 && current >= 0);
assert.ok(Math.abs(f.intervals[0][0] + f.intervals[0][1] - 1) < 1e-10);
assert.equal(f.clarity, 0);
previous = current;
}
assert.equal(length(decryptionFrame(38.84)), 0);
assert.equal(decryptionFrame(38.84).clarity, 0);
assert.equal(decryptionFrame(39.56).clarity, 1);
assert.ok(length(decryptionFrame(34.64)) > 0.5, "Joining is eased, not linear");
const a = new DecryptionController();
a.enter();
a.update(3, false, false);
assert.equal(a.frame.phase, "waiting");
a.update(0, true, false);
for (let i = 0; i < 120; i++) a.update(1 / 30, true, false);
assert.equal(a.clarity, 1);
a.leave();
assert.equal(a.frame.intervals.length, 0);
a.update(0.1, false, false);
const returning = a.clarity;
a.enter();
a.update(0, true, false);
assert.equal(a.clarity, returning);
a.update(0.1, true, true);
assert.equal(a.clarity, 1);
assert.equal(a.frame.intervals.length, 0);
a.select();
assert.equal(a.clarity, 0);
a.update(0, false, false, 35);
assert.equal(a.frame.phase, "joining");
a.update(0, false, false, 39.56);
assert.equal(a.clarity, 1);
a.update(0, false, false, 34);
assert.equal(a.clarity, 0, "Reference seeking is reversible");
const appearance = new CardAppearance();
const high = new THREE.MeshPhysicalMaterial({
transmission: 0.9,
thickness: 0.12,
attenuationDistance: 2,
});
const low = high.clone();
low.thickness = 0.25;
appearance.register("Frosted_Polymer", high, low);
const group = new THREE.Group(),
mesh = new THREE.Mesh(new THREE.BoxGeometry(), high);
mesh.userData.surface = "Frosted_Polymer";
group.add(mesh);
appearance.prepare(group);
appearance.apply(group, 1);
const shader = {
uniforms: {},
vertexShader: "#include <begin_vertex>\n#include <project_vertex>",
fragmentShader: "#include <transmission_pars_fragment>\n#include <color_fragment>\n#include <roughnessmap_fragment>",
};
mesh.material.onBeforeCompile(shader);
assert.ok(shader.vertexShader.includes("vArchiveProjectedAxis = 1.85"));
assert.ok(shader.fragmentShader.includes("float lod = archiveTransmissionLod(roughness, ior, transmissionSamplerSize);"));
const part = new THREE.Group();
group.add(part);
part.add(mesh);
appearance.setClarity(group, 1);
assert.equal(shader.uniforms.archiveClarity.value, 1);
assert.ok(Math.abs(mesh.material.thickness - 0.018) < 1e-10);
appearance.setClarity(group, 0);
assert.equal(shader.uniforms.archiveClarity.value, 0);
assert.equal(mesh.material.thickness, 0.12);
const bytes = await readFile(
new URL("../public/assets/archive-assembly.glb", import.meta.url),
);
const asset = (
await new GLTFLoader().parseAsync(
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
"",
)
).scene;
asset.updateMatrixWorld(true);
let interiors = 0,
maxDepth = -Infinity,
minDepth = Infinity;
asset.traverse((mesh) => {
if (
!mesh.isMesh ||
!["optical-core", "optical-lenses"].includes(mesh.userData.assemblyPart)
)
return;
const box = new THREE.Box3().setFromObject(mesh);
minDepth = Math.min(minDepth, box.min.z);
maxDepth = Math.max(maxDepth, box.max.z);
interiors++;
assert.ok(
box.min.z > -0.038 && box.max.z < 0.174,
"Interior stays between substrate and cover",
);
});
assert.ok(interiors >= 6);
// Perspective depth resolution, at the actual detail distance and 24-bit depth.
const step = (near, distance) =>
(distance * distance * (300 - near)) / (300 * near * (2 ** 24 - 1));
assert.ok(step(5, 72) < step(0.1, 72) / 50);
console.log(
JSON.stringify(
{
passed: true,
interiorMeshes: interiors,
interiorDepth: [minDepth, maxDepth],
depthStepBefore: step(0.1, 72),
depthStepAfter: step(5, 72),
},
null,
2,
),
);