• あ

    拿dsh+v4pro max测了一下牙医的大象牙膏测试:

    图片

    代码:```
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大象牙膏 · Elephant Toothpaste 3D 化学实验演示</title>
    <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1630; }
    #container { position: fixed; inset: 0; }
    #container canvas { display: block; }

    #vignette {
    position: fixed; inset: 0; pointer-events: none; z-index: 2;
    background: radial-gradient(ellipse at 50% 42%, transparent 58%, rgba(24, 10, 34, 0.38) 100%);
    }

    #hint {
    position: fixed; top: 14px; left: 16px; z-index: 6;
    color: rgba(255, 255, 255, 0.78); font-size: 13px; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
    background: rgba(22, 20, 40, 0.45); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
    padding: 8px 14px; border-radius: 20px; pointer-events: none;
    border: 1px solid rgba(255, 255, 255, 0.10);
    }

    footer {
    position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 10;
    display: flex; gap: 20px; align-items: center;
    background: rgba(24, 21, 42, 0.55);
    backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
    border: 1px solid rgba(255, 255, 255, 0.14);
    border-radius: 999px; padding: 10px 18px 10px 24px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
    font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
    max-width: 94vw;
    }
    footer .title { color: #fff; font-size: 15px; font-weight: 600; white-space: nowrap; }
    footer .title .sub { opacity: 0.5; font-weight: 400; font-size: 12px; margin-left: 8px; }
    #status { color: #ffd6ea; font-size: 13px; opacity: 0.88; min-width: 150px; text-align: center; white-space: nowrap; }

    #btn {
    border: none; cursor: pointer;
    font-family: inherit; font-size: 15px; font-weight: 700; color: #fff;
    padding: 10px 24px; border-radius: 999px;
    background: linear-gradient(135deg, #ff4fae 0%, #ff2d8f 55%, #e01a7c 100%);
    box-shadow: 0 4px 18px rgba(255, 60, 150, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.35);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
    white-space: nowrap;
    }
    #btn:hover { transform: scale(1.06); box-shadow: 0 6px 26px rgba(255, 60, 150, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.35); }
    #btn:active { transform: scale(0.96); }

    @media (max-width: 700px) {
    footer { flex-direction: column; gap: 8px; border-radius: 26px; padding: 14px 20px; }
    #hint { display: none; }
    }
    </style>
    </head>
    <body>
    <div id="container"></div>
    <div id="vignette"></div>
    <div id="hint">🖱️ 左键拖拽旋转 &nbsp;·&nbsp; 滚轮缩放 &nbsp;·&nbsp; 右键平移</div>

    <footer>
    <div class="title">🐘 大象牙膏 <span class="sub">Elephant Toothpaste · 3D</span></div>
    <div id="status">💤 准备就绪 · 点击按钮开始实验</div>
    <button id="btn">🧪 开始实验</button>
    </footer>

    <script type="importmap">
    {
    "imports": {
    "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
    }
    }
    </script>

    <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';

    /* ============================================================
    * 常量与烧瓶几何函数
    * 烧瓶(Erlenmeyer flask)轮廓(单位统一):
    内壁半径: y < 20 时 R(y) = 10 - 0.4y
    * y >= 20 时 R(y) = 2 (瓶颈圆柱)
    * 瓶身高 ~31.85,壁厚 ~0.18,瓶口在 y≈31.85 开放
    ============================================================ /
    const L0 = 10; // 初始液面高度(约为瓶高的 1/3)
    const MOUTH_Y = 31.85; // 瓶口高度(粒子在此处飞出)
    const G = 9.8;
    const N_PARTICLES = 2000; // 泡沫粒子总数
    const GRID_CELL = 2.3;

    function ss(x) { x = Math.min(1, Math.max(0, x)); return x x (3 - 2 * x); }
    function innerR(y) { return y < 20 ? 10 - 0.4 * y : 2; }
    / 外壁半径(与下方 LatheGeometry 轮廓一致,用于瓶外泡沫与瓶壁的碰撞) /
    function outerR(y) {
    if (y < 17.6) return 10.18 - 0.4 * y;
    if (y < 20.8) return 3.14 + (2.18 - 3.14) * ss((y - 17.6) / 3.2);
    if (y < 30.4) return 2.18;
    if (y < 31.05) return 2.18 + 0.15 * ss((y - 30.4) / 0.65);
    if (y < 31.42) return 2.33 + 0.19 * ss((y - 31.05) / 0.37);
    if (y < 31.72) return 2.52 - 0.10 * ss((y - 31.42) / 0.30);
    if (y < 31.85) return 2.42 - 0.20 * ss((y - 31.72) / 0.13);
    return 2.22;
    }
    / 反应压力:初期猛烈 → 随时间衰减(反应物耗尽) /
    function pressure(t) {
    if (t < 0) return 0;
    const p = t < 4 ? 1 : Math.max(0.14, 1 - 0.86 * (t - 4) / 8);
    const fade = t < 12 ? 1 : Math.max(0, (14 - t) / 2);
    return p * fade;
    }

    / ============================================================ 渲染器 ============================================================ /
    const container = document.getElementById('container');
    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.05;
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    container.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    scene.fog = new THREE.Fog(0xf0a9c2, 220, 950);

    / 摄像机:斜 45° 对角线俯视场景中心(略微收低以同时容纳天空、喷柱与堆积层) /
    const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 3000);
    camera.position.set(95, 89, 95);
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 5, 0);
    controls.enableDamping = true;
    controls.dampingFactor = 0.06;
    controls.minDistance = 45;
    controls.maxDistance = 420;
    controls.minPolarAngle = 0.05;
    controls.maxPolarAngle = 1.5;
    controls.update();

    window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    });

    / 环境反射(玻璃 / 液体 / 地面的微反射) /
    const pmrem = new THREE.PMREMGenerator(renderer);
    scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
    pmrem.dispose();

    / ============================================================ 晨曦天空盒 ============================================================ /
    const sunDir = new THREE.Vector3(-120, 30, -120).normalize(); // 与主光源方向一致(晨曦低角度)
    const sky = new THREE.Mesh(
    new THREE.SphereGeometry(1200, 48, 24),
    new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    fog: false,
    uniforms: { uSunDir: { value: sunDir } },
    vertexShader: `
    varying vec3 vDir;
    void main() {
    vDir = position;
    gl_Position = projectionMatrix modelViewMatrix vec4(position, 1.0);
    }`,
    fragmentShader: `
    varying vec3 vDir;
    uniform vec3 uSunDir;
    void main() {
    vec3 d = normalize(vDir);
    float h = d.y;
    vec3 cTop = vec3(0.28, 0.42, 0.80); // 顶部蓝
    vec3 cMid = vec3(0.55, 0.55, 0.90); // 中部过渡(近地平线的淡蓝)
    vec3 cHor = vec3(1.00, 0.76, 0.80); // 地平线粉
    vec3 cLow = vec3(0.90, 0.54, 0.72); // 地平线以下暖粉
    vec3 col = mix(cHor, cMid, smoothstep(-0.05, 0.12, h));
    col = mix(col, cTop, smoothstep(0.04, 0.30, h));
    col = mix(cLow, col, smoothstep(-0.30, -0.02, h));
    float s = max(dot(d, uSunDir), 0.0);
    col += vec3(1.0, 0.88, 0.72) (pow(s, 1500.0) 1.0 + pow(s, 120.0) 0.16 + pow(s, 10.0) 0.05);
    gl_FragColor = vec4(col, 1.0);
    }`
    })
    );
    sky.renderOrder = -10;
    sky.frustumCulled = false;
    scene.add(sky);

    / ============================================================ 光照 ============================================================ /
    scene.add(new THREE.HemisphereLight(0xcfe0ff, 0xeec0d0, 0.8)); // 天光/地光
    scene.add(new THREE.AmbientLight(0xffe3ee, 0.32)); // 环境光补光,无纯黑

    const sun = new THREE.DirectionalLight(0xffe8cf, 2.9); // 主光源(低角度晨曦日光)
    sun.position.set(-120, 30, -120);
    sun.target.position.set(0, 5, 0);
    sun.castShadow = true;
    sun.shadow.mapSize.set(4096, 4096);
    sun.shadow.camera.left = -260;
    sun.shadow.camera.right = 260;
    sun.shadow.camera.top = 260;
    sun.shadow.camera.bottom = -260;
    sun.shadow.camera.near = 20;
    sun.shadow.camera.far = 700;
    sun.shadow.bias = -0.0005;
    sun.shadow.normalBias = 0.04;
    scene.add(sun);
    scene.add(sun.target);

    const fill = new THREE.DirectionalLight(0xa8b8ff, 0.55); // 冷色补光
    fill.position.set(-70, 60, -90);
    scene.add(fill);

    const glowLight = new THREE.PointLight(0xff4fb0, 0, 90, 1.7); // 液面处的反应辉光
    glowLight.position.set(0, L0 + 2, 0);
    scene.add(glowLight);

    / ============================================================ 地面 ============================================================ /
    const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(1000, 1000),
    new THREE.MeshStandardMaterial({ color: 0x82878f, roughness: 0.15, metalness: 0.06, envMapIntensity: 0.35 })
    );
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    scene.add(ground);

    / ============================================================ 玻璃锥形瓶 ============================================================ /
    function buildFlaskGeometry() {
    const pts = [];
    pts.push(new THREE.Vector2(0, 0)); // 扁平瓶底
    pts.push(new THREE.Vector2(10.18, 0));
    pts.push(new THREE.Vector2(10.18, 0.55));
    for (let y = 1; y <= 17.6; y += 0.8) pts.push(new THREE.Vector2(10.18 - 0.4 * y, y)); // 圆锥瓶身
    for (let y = 18; y <= 20.8; y += 0.4) { // 圆润的肩部
    const t = ss((y - 17.6) / 3.2);
    pts.push(new THREE.Vector2(3.14 + (2.18 - 3.14) * t, y));
    }
    for (let y = 21.4; y <= 29.4; y += 1) pts.push(new THREE.Vector2(2.18, y)); // 圆柱瓶颈
    pts.push(new THREE.Vector2(2.18, 30.4));
    pts.push(new THREE.Vector2(2.33, 31.05)); // 瓶口外翻
    pts.push(new THREE.Vector2(2.52, 31.42));
    pts.push(new THREE.Vector2(2.42, 31.72)); // 圆润翻边
    pts.push(new THREE.Vector2(2.22, 31.85));
    pts.push(new THREE.Vector2(2.04, 31.62)); // 瓶口内缘
    pts.push(new THREE.Vector2(2.0, 31.3)); // 瓶颈内壁
    pts.push(new THREE.Vector2(2.0, 30.6));
    pts.push(new THREE.Vector2(2.0, 29.7));
    return new THREE.LatheGeometry(pts, 128);
    }

    const glassMat = new THREE.MeshPhysicalMaterial({
    color: 0xffffff,
    metalness: 0,
    roughness: 0.03, // 低粗糙度
    ior: 1.5, // 玻璃折射率
    transmission: 1, // 高透射率
    thickness: 0.55,
    transparent: true,
    opacity: 0.34, // 关键:保留瓶内透明泡沫/液体透出,同时叠加折射背景
    envMapIntensity: 1.15, // 环境微反射
    clearcoat: 0.3,
    clearcoatRoughness: 0.08,
    specularIntensity: 1.0,
    side: THREE.DoubleSide,
    });
    const flask = new THREE.Mesh(buildFlaskGeometry(), glassMat);
    flask.castShadow = true;
    flask.renderOrder = 3;
    scene.add(flask);

    / ============================================================ 荧光粉色液体(液面随反应下降) ============================================================ /
    const liquidMat = new THREE.MeshPhysicalMaterial({
    color: 0xff2f93,
    metalness: 0,
    roughness: 0.07,
    ior: 1.33, // 液体折射率
    transmission: 1,
    thickness: 2.0,
    attenuationColor: 0xff3fa8,
    attenuationDistance: 0.8,
    transparent: true,
    opacity: 0.97,
    emissive: 0x47062a,
    emissiveIntensity: 0.55, // 荧光感
    envMapIntensity: 0.6,
    });

    function buildLiquidGeometry(L) {
    const pts = [new THREE.Vector2(0, 0), new THREE.Vector2(10, 0)];
    for (let y = 0.5; y < L; y += 0.5) pts.push(new THREE.Vector2(10 - 0.4 * y, y));
    pts.push(new THREE.Vector2(10 - 0.4 * L, L)); // 内壁贴合
    pts.push(new THREE.Vector2(0, L)); // 平整液面
    return new THREE.LatheGeometry(pts, 80);
    }

    const liquid = new THREE.Mesh(buildLiquidGeometry(L0), liquidMat);
    liquid.renderOrder = 1;
    liquid.castShadow = true;
    scene.add(liquid);

    function setLiquidLevel(L) {
    const g = buildLiquidGeometry(L);
    liquid.geometry.dispose();
    liquid.geometry = g;
    }

    / ============================================================ 泡沫噪点纹理(多孔质感) ============================================================ /
    function makeFoamTexture() {
    const S = 128;
    const cv = document.createElement('canvas');
    cv.width = cv.height = S;
    const ctx = cv.getContext('2d');
    ctx.fillStyle = '#f7f2f8';
    ctx.fillRect(0, 0, S, S);
    for (let i = 0; i < 1100; i++) { // 深色孔隙
    const x = Math.random() S, y = Math.random() S;
    const r = 1.5 + Math.random() * 15;
    const g = ctx.createRadialGradient(x, y, 0, x, y, r);
    const a = 0.08 + Math.random() * 0.22;
    g.addColorStop(0, 'rgba(40,20,50,' + a + ')');
    g.addColorStop(1, 'rgba(40,20,50,0)');
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
    }
    for (let i = 0; i < 500; i++) { // 高光小气泡
    const x = Math.random() S, y = Math.random() S;
    const r = 1 + Math.random() * 9;
    const g = ctx.createRadialGradient(x, y, 0, x, y, r);
    const a = 0.06 + Math.random() * 0.18;
    g.addColorStop(0, 'rgba(255,255,255,' + a + ')');
    g.addColorStop(1, 'rgba(255,255,255,0)');
    ctx.fillStyle = g;
    ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
    }
    const tex = new THREE.CanvasTexture(cv);
    tex.colorSpace = THREE.SRGBColorSpace;
    return tex;
    }

    / ============================================================ 泡沫粒子系统(2000 个 InstancedMesh 球体) ============================================================ /
    function randSize() {
    let r = 0.2 + 0.55 * Math.pow(Math.random(), 1.6);
    if (Math.random() < 0.05) r = 0.8 + 0.35 * Math.random();
    return Math.min(r, 1.15);
    }

    class FoamSystem {
    constructor(n, scene) {
    this.N = n;
    this.cell = GRID_CELL;
    this.grid = new Map();
    this.parts = [];
    for (let i = 0; i < n; i++) {
    this.parts.push({
    x: 0, y: -2000, z: 0, vx: 0, vy: 0, vz: 0,
    r: randSize(), state: 0,
    br: 0.78 + 0.45 * Math.random(),
    hue: (Math.random() - 0.5) * 0.1,
    landT: 0, sqY: 1, sqX: 1, rest: 0, clingV: 0
    });
    }
    this.spawned = 0;
    this.spawnAcc = 0;

    const geo = new THREE.IcosahedronGeometry(1, 2);

    const mat = new THREE.MeshStandardMaterial({
    color: 0xffffff, // 基色白:逐粒子粉色由 instanceColor 提供
    map: makeFoamTexture(), // 多孔泡沫纹理
    roughness: 0.42,
    metalness: 0.02,
    emissive: 0xff1e7e,
    emissiveIntensity: 0.3, // 荧光亮度
    transparent: true,
    opacity: 0.95,
    depthWrite: true, // 深度参与排序:保证与玻璃/液体的正确遮挡
    vertexColors: true, // 启用 instanceColor 逐粒子颜色
    });

    / 注入:软边缘融合(近似相互交融的泡沫)、边缘提亮、边缘荧光、纹理采样缩放 /
    mat.onBeforeCompile = (shader) => {
    shader.vertexShader = shader.vertexShader
    .replace('#include <uv_vertex>', '#include <uv_vertex>\nvMapUv = uv * 0.62 + 0.19;');
    shader.fragmentShader = shader.fragmentShader
    .replace('#include <emissivemap_fragment>', '#include <emissivemap_fragment>\n{\n float _ndv = clamp(dot(normalize(vNormal), normalize(-vViewPosition)), 0.0, 1.0);\n diffuseColor.a = mix(0.22, 1.0, pow(_ndv, 1.35));\n diffuseColor.rgb = mix(0.85, 1.12, pow(1.0 - _ndv, 2.0));\n vec3 _Nv = normalize(vNormal);\n vec3 _Vv = normalize(-vViewPosition);\n float _rim = pow(1.0 - max(dot(_Nv, _Vv), 0.0), 2.6);\n totalEmissiveRadiance += vec3(1.0, 0.3, 0.6) _rim 0.5;\n}');
    };

    this.mesh = new THREE.InstancedMesh(geo, mat, n);
    this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.mesh.castShadow = true;
    this.mesh.receiveShadow = false;
    this.mesh.frustumCulled = false;
    this.mesh.renderOrder = 2;
    scene.add(this.mesh);

    / 逐粒子荧光粉亮度/色相变化(instanceColor,three 内建逐实例属性) /
    this.colorTmp = new THREE.Color();
    for (let i = 0; i < n; i++) {
    const p = this.parts[i];
    this.colorTmp.setRGB(
    Math.min(1, p.br * (1 + p.hue)),
    Math.min(1, 0.31 * p.br),
    Math.min(1, 0.71 p.br (1 - p.hue))
    );
    this.mesh.setColorAt(i, this.colorTmp);
    }
    if (this.mesh.instanceColor) this.mesh.instanceColor.needsUpdate = true;

    this.dummy = new THREE.Object3D();
    this.writeMatrices();
    }

    reset() {
    for (const p of this.parts) {
    p.state = 0; p.landT = 0; p.sqY = 1; p.sqX = 1; p.rest = 0; p.clingV = 0;
    p.r = randSize();
    p.br = 0.78 + 0.45 * Math.random();
    p.hue = (Math.random() - 0.5) * 0.1;
    }
    for (let i = 0; i < this.N; i++) {
    const p = this.parts[i];
    this.colorTmp.setRGB(
    Math.min(1, p.br * (1 + p.hue)),
    Math.min(1, 0.31 * p.br),
    Math.min(1, 0.71 p.br (1 - p.hue))
    );
    this.mesh.setColorAt(i, this.colorTmp);
    }
    if (this.mesh.instanceColor) this.mesh.instanceColor.needsUpdate = true;
    this.spawned = 0;
    this.spawnAcc = 0;
    this.writeMatrices();
    }

    countActive() {
    let c = 0;
    for (const p of this.parts) if (p.state) c++;
    return c;
    }

    writeMatrices() {
    const m = this.mesh;
    const d = this.dummy;
    for (let i = 0; i < this.N; i++) {
    const p = this.parts[i];
    if (!p.state) {
    d.position.set(0, -2000, 0);
    d.scale.set(0, 0, 0);
    } else {
    d.position.set(p.x, p.y, p.z);
    const sx = p.state === 3 ? p.sqX : 1;
    const sy = p.state === 3 ? p.sqY : 1;
    d.scale.set(p.r sx, p.r sy, p.r * sx);
    }
    d.updateMatrix();
    m.setMatrixAt(i, d.matrix);
    }
    m.instanceMatrix.needsUpdate = true;
    }

    hash(cx, cy, cz) {
    return (cx 73856093 ^ cy 19349663 ^ cz * 83492791) | 0;
    }

    spawnOne(L, pres) {
    const p = this.parts[this.spawned++];
    const ang = Math.random() Math.PI 2;
    const rr = Math.sqrt(Math.random()) * Math.min(2.6, innerR(L) - 1.1);
    p.x = Math.cos(ang) * rr;
    p.z = Math.sin(ang) * rr;
    p.y = L + 0.15 + Math.random() * 0.6;
    p.vx = (Math.random() - 0.5) * 2;
    p.vz = (Math.random() - 0.5) * 2;
    p.vy = 8 + Math.random() 8 + 3 pres;
    p.state = 1;
    p.landT = 0; p.sqY = 1; p.sqX = 1; p.rest = 0;
    }

    land(p) {
    p.state = 3;
    p.landT = 0;
    p.vy = 0;
    p.vx = 0.2; p.vz = 0.2;
    let best = p.r * 0.45;
    const cx = Math.floor(p.x / this.cell), cz = Math.floor(p.z / this.cell);
    const cy0 = Math.floor(p.y / this.cell);
    for (let dx = -1; dx <= 1; dx++) {
    for (let dz = -1; dz <= 1; dz++) {
    for (let dy = 0; dy <= 2; dy++) { // 扫描自身及上方网格层,找到堆积泡沫的最高点
    const b = this.grid.get(this.hash(cx + dx, cy0 + dy, cz + dz));
    if (!b) continue;
    for (const j of b) {
    const q = this.parts[j];
    if (q.state !== 3) continue;
    const ddx = p.x - q.x, ddz = p.z - q.z;
    const reach = p.r 1.5 + q.r 1.5;
    if (ddx ddx + ddz ddz < reach * reach) {
    const h = q.y + q.r q.sqY 0.85 + p.r * 0.45;
    if (h > best) best = h;
    }
    }
    }
    }
    }
    p.rest = best;
    p.sqY = 1; p.sqX = 1;
    }

    update(dt, t, pres, L) {
    const boost = 1 + 0.15 * Math.exp(-t / 1.3);
    const N = this.N;
    const cell = this.cell;

    / --- 生成(速率随压力衰减;粒子池耗尽后停止) --- /
    if (pres > 0.06 && this.spawned < N) {
    this.spawnAcc += (250 pres + 30) dt;
    while (this.spawnAcc >= 1 && this.spawned < N) {
    this.spawnAcc -= 1;
    this.spawnOne(L, pres);
    }
    }

    / --- 空间哈希网格 --- /
    this.grid.clear();
    for (let i = 0; i < N; i++) {
    const p = this.parts[i];
    if (!p.state) continue;
    const key = this.hash(Math.floor(p.x / cell), Math.floor(p.y / cell), Math.floor(p.z / cell));
    let b = this.grid.get(key);
    if (!b) this.grid.set(key, b = []);
    b.push(i);
    }

    const dt60 = Math.min(dt * 60, 3);

    / --- 粒子间轻微排斥(泡沫膨胀感 / 堆积摊开) --- /
    for (let i = 0; i < N; i++) {
    const p = this.parts[i];
    if (!p.state) continue;
    const cx = Math.floor(p.x / cell), cy = Math.floor(p.y / cell), cz = Math.floor(p.z / cell);
    let cnt = 0;
    outer:
    for (let dx = -1; dx <= 1; dx++) {
    for (let dy = -1; dy <= 1; dy++) {
    for (let dz = -1; dz <= 1; dz++) {
    const b = this.grid.get(this.hash(cx + dx, cy + dy, cz + dz));
    if (!b) continue;
    for (let bi = 0; bi < b.length; bi++) {
    const j = b[bi];
    if (j === i) continue;
    const q = this.parts[j];
    const ddx = p.x - q.x, ddy = p.y - q.y, ddz = p.z - q.z;
    const rad = p.r + q.r;
    const d2 = ddx ddx + ddy ddy + ddz * ddz;
    if (d2 >= rad * rad || d2 < 1e-9) continue;
    const d = Math.sqrt(d2);
    const overlap = (rad - d) / rad;
    let imp = 0.34 overlap dt60;
    if (imp > 0.75) imp = 0.75;
    const soft = (p.state === 3 && q.state === 3) ? 0.4 : 1;
    const w = q.r / rad;
    p.vx += (ddx / d) imp w * soft;
    p.vy += (ddy / d) imp w * soft;
    p.vz += (ddz / d) imp w * soft;
    cnt++;
    if (cnt >= 10) break outer;
    }
    }
    }
    }
    }

    / --- 状态力学 + lao1 --- /
    for (let i = 0; i < N; i++) {
    const p = this.parts[i];
    switch (p.state) {
    case 0: continue;
    case 1: { / 瓶内:气压驱动 + 文丘里喷嘴加速(∝ 1/截面积 ∝ 1/R²) /
    const R = innerR(p.y);
    const areaF = 100 / (R * R);
    p.vy += 42 pres areaF * dt;
    p.vy -= G * dt;
    p.vy = 1 - 0.35 dt;
    p.vx = 1 - 2.0 dt; p.vz = 1 - 2.0 dt;
    p.vx += (Math.random() - 0.5) 5 dt;
    p.vz += (Math.random() - 0.5) 5 dt;
    const vcap = 22 + 30 pres boost;
    if (p.vy > vcap) p.vy = vcap;
    break;
    }
    case 2: { / 空中:重力 + 阻力;上升时保持紧密泡沫柱,近顶点才扩散 /
    p.vy -= G * dt;
    const kd = 0.13;
    p.vx -= kd p.vx dt;
    p.vy -= kd p.vy dt;
    p.vz -= kd p.vz dt;
    if (p.vy > 13) {
    p.vx = 1 - 3.0 dt;
    p.vz = 1 - 3.0 dt;
    p.vx += (Math.random() - 0.5) 2.6 dt;
    p.vz += (Math.random() - 0.5) 2.6 dt;
    } else {
    p.vx += (Math.random() - 0.5) 8.5 dt;
    p.vz += (Math.random() - 0.5) 8.5 dt;
    }
    break;
    }
    case 3: { / 落地:摊开 + 垂直压扁 + 水平扩展(气体溢出) /
    p.vx = 1 - 7 dt;
    p.vz = 1 - 7 dt;
    p.vy = 0;
    if (p.landT < 0.4) {
    p.landT += dt;
    const tt = Math.min(p.landT / 0.4, 1);
    const e = tt tt (3 - 2 * tt);
    p.sqY = 1 - 0.58 * e;
    p.sqX = 1 + 0.36 * e;
    }
    if (p.landT < 1.4) p.y += (p.rest - p.y) Math.min(1, 7 dt);
    break;
    }
    case 4: { / 挂壁滑动 /
    p.vx = 0; p.vz = 0; p.vy = 0;
    break;
    }
    }
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.z += p.vz * dt;
    }

    / --- 碰撞与约束 --- /
    for (let i = 0; i < N; i++) {
    const p = this.parts[i];
    switch (p.state) {
    case 0: continue;
    case 1: { / 瓶内壁数学边界 R(y):绝不允许穿模,碰撞沿壁向上滑动 /
    const R = innerR(p.y);
    const rr = Math.hypot(p.x, p.z);
    const lim = Math.max(R - p.r * 0.9, 0.05);
    if (rr > lim) {
    const nx = rr > 1e-6 ? p.x / rr : 1;
    const nz = rr > 1e-6 ? p.z / rr : 0;
    p.x = nx lim; p.z = nz lim;
    const vr = p.vx nx + p.vz nz;
    if (vr > 0) {
    p.vx -= 1.35 vr nx;
    p.vz -= 1.35 vr nz;
    p.vy += vr 0.55; / 沿瓶壁向上滑动 */
    }
    }
    if (p.y < L + p.r 0.6) { / 不沉入液体 */
    p.y = L + p.r * 0.6;
    if (p.vy < 0) p.vy = 0;
    }
    if (p.y >= MOUTH_Y) { / 飞出瓶口 → 喷射流 /
    p.state = 2;
    if (Math.random() < 0.22) { / 少部分沿瓶口边缘溅出 /
    p.vx += (Math.random() - 0.5) * 6;
    p.vz += (Math.random() - 0.5) * 6;
    p.vy = 0.45 + 0.35 Math.random();
    }
    }
    break;
    }
    case 2: {
    if (p.y <= p.r 0.85) { / 撞地:弹性反弹 → 摊开堆积 */
    p.y = p.r * 0.85;
    const impact = -p.vy;
    if (impact > 4.5) {
    p.vy = Math.min(impact * 0.25, 13);
    p.vx = 0.4; p.vz = 0.4;
    } else {
    this.land(p);
    }
    }
    if (p.state === 2 && p.y < 31.9) { / 瓶壁碰撞 /
    const rr = Math.hypot(p.x, p.z);
    const Rin = innerR(p.y);
    if (rr > Rin) {
    const Rout = outerR(p.y);
    if (rr < Rout + p.r * 0.7) {
    const nx = rr > 1e-6 ? p.x / rr : 1, nz = rr > 1e-6 ? p.z / rr : 0;
    const vr = p.vx nx + p.vz nz;
    if (vr < 0 || rr > Rout) { / 弹开 /
    p.vx -= 1.5 vr nx;
    p.vz -= 1.5 vr nz;
    p.vy *= 0.72;
    }
    const Rpush = Rout + p.r * 0.5;
    p.x = nx Rpush; p.z = nz Rpush;
    if (p.r < 0.34 || Math.random() < 0.17) { / 挂壁:小颗粒直接粘住,其余缓慢滑落 /
    p.state = 4;
    p.clingV = p.r < 0.34 ? 0 : 0.5 + Math.random() * 0.9;
    }
    }
    } else if (p.vy < 0) { / 落回瓶口内部 → 重新进入喷嘴循环 /
    p.state = 1;
    }
    }
    break;
    }
    case 3: {
    if (p.y < p.r p.sqY 0.8) p.y = p.r p.sqY 0.8;
    if (p.y < 31.9) { / 堆积层绕开瓶底 /
    const rr = Math.hypot(p.x, p.z);
    const Rneed = innerR(p.y) + p.r * 0.8;
    if (rr < Rneed && rr > 1e-6) {
    p.x = Rneed / rr; p.z = Rneed / rr;
    }
    }
    break;
    }
    case 4: {
    if (Math.random() < 0.0009) { / 偶尔脱落 /
    p.state = 2;
    p.vy = -0.6;
    const rr = Math.hypot(p.x, p.z) || 1;
    p.vx = (p.x / rr) * 0.8;
    p.vz = (p.z / rr) * 0.8;
    break;
    }
    p.y -= p.clingV dt; / 沿瓶壁缓慢下滑 */
    if (p.y < p.r * 0.85) { this.land(p); break; }
    const R = outerR(p.y) + p.r * 0.45;
    const rr = Math.hypot(p.x, p.z) || 1e-4;
    p.x = R / rr; p.z = R / rr;
    break;
    }
    }
    }

    this.writeMatrices();
    }
    }

    const foam = new FoamSystem(N_PARTICLES, scene);

    / ============================================================ UI 与主循环 ============================================================ /
    const btn = document.getElementById('btn');
    const statusEl = document.getElementById('status');
    const clock = new THREE.Clock();
    let started = false, tElapsed = 0, lastL = L0, statusTimer = 0;

    function startEruption() {
    foam.reset();
    tElapsed = 0;
    lastL = L0;
    setLiquidLevel(L0);
    started = true;
    btn.textContent = '🔄 重新实验';
    }
    btn.addEventListener('click', startEruption);

    function updateStatus(pres) {
    if (!started) statusEl.textContent = '💤 准备就绪 · 点击按钮开始实验';
    else if (pres > 0.25) statusEl.textContent = '⚗️ 剧烈反应 · 泡沫喷发中';
    else if (pres > 0.05) statusEl.textContent = '🌫️ 反应减弱 · 泡沫回落';
    else if (tElapsed < 17) statusEl.textContent = '🧽 泡沫堆积成型中…';
    else statusEl.textContent = '✅ 反应完成 · 泡沫已堆积';
    }

    function animate() {
    const dt = Math.min(clock.getDelta(), 0.05);
    let pres = 0, L = L0;
    if (started) {
    tElapsed += dt;
    pres = pressure(tElapsed);
    L = 2 + 8 Math.exp(-tElapsed / 4.2); / 液面逐渐下降(反应物消耗) */
    }
    foam.update(dt, tElapsed, pres, L);
    if (Math.abs(L - lastL) > 0.03) {
    lastL = L;
    setLiquidLevel(L);
    }
    glowLight.intensity = started ? 70 * pres + 2 : 0;
    glowLight.position.set(0, L + 2, 0);
    statusTimer -= dt;
    if (statusTimer <= 0) { statusTimer = 0.25; updateStatus(pres); }
    controls.update();
    renderer.render(scene, camera);
    }
    renderer.setAnimationLoop(animate);

    / 测试钩子(无头冒烟测试用) /
    window.__demo = {
    started: () => started,
    time: () => tElapsed,
    pressure: () => (started ? pressure(tElapsed) : 0),
    activeCount: () => foam.countActive(),
    start: startEruption,
    fastForward(seconds) {
    const steps = Math.round(seconds * 60);
    const dt = 1 / 60;
    for (let s = 0; s < steps; s++) {
    tElapsed += dt;
    const pres = pressure(tElapsed);
    const L = 2 + 8 * Math.exp(-tElapsed / 4.2);
    foam.update(dt, tElapsed, pres, L);
    if (Math.abs(L - lastL) > 0.03) { lastL = L; setLiquidLevel(L); }
    }
    const pres = started ? pressure(tElapsed) : 0;
    const L = started ? 2 + 8 * Math.exp(-tElapsed / 4.2) : L0;
    glowLight.intensity = started ? 70 * pres + 2 : 0;
    glowLight.position.set(0, L + 2, 0);
    }
    };
    </script>
    </body>
    </html>
    ```猜猜跑了多久 ^-^,究竟是我的问题还是它的问题

    图片

    主楼
  • 远坂凛的宠物

    图动不了,所以结论这算好还是坏啊 ^-^

    #1
  • biribiri

    目前看来这个dsh就是一个很普通的harness,根本没有和v4pga有什么特化,所以之前测到v4pga会有的问题dsh上一样不会更好

    #2
  • cymorian

    图片

    这到底是在测啥的

    #3
  • あ

    B站一个UP主的一个测试,主要是测试模型的3D渲染、物理与特效模拟的能力的

    #4

发表回复

登录后回复