dsh + v4pro max 牙医大象牙膏测试

2026-08-13 22:33 1

拿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.4*y
* 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;
}
}
}
}
}

/* --- 状态力学 + 积分 --- */
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>

猜猜跑了多久 ^-^,究竟是我的问题还是它的问题

最新回复 (4)
  • 远坂凛的宠物 08-13 22:35
    1

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

  • biribiri 08-13 22:36
    2

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

  • cymorian 08-13 22:38
    3


    这到底是在测啥的

  • 楼主 08-13 23:23
    4

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

* 帖子来源Linux.do
返回