大象牙膏测试
---------------
(CC-BY-NC-SA 4.0 by karminski-牙医)
请使用 three.js 实现一个逼真的"大象牙膏"化学实验3D演示。所有代码(包括HTML, CSS, JavaScript)都必须封装在一个独立的HTML文件中。
**场景设置:**
1. **平面:** 创建一个尺寸为1000*1000的灰色、光滑的水平平面它能接收阴影。
2. **三角烧瓶:**
* **形状:** 在平面中心放置一个透明的玻璃三角烧瓶。烧瓶应具有清晰的轮廓:圆柱形的颈部、逐渐变宽的圆锥形瓶身和扁平的底部。请勿使用简单的圆锥体代替。
* **材质:** 烧瓶材质应为高度透明的玻璃,具有适当的高透射率,较低的粗糙度,以及正确的折射率来模拟玻璃。设置 ```{ color: 0xffffff, transparent: true, opacity: 0.9, roughness: 0.95, metalness: 0.35, clearcoat: 1.0, clearcoatRoughness: 0.03, transmission: 0.95, ior: 1.5, side: THREE.DoubleSid}```应能看到背景和透过液体的光线折射效果。
* **液体:** 烧瓶内预先装有约三分之一高度的荧光粉色液体。液体表面应平整,并与烧瓶内壁贴合。注意参考三角烧瓶建模的形状,理想的做法是复制三角烧瓶的部分锥形的形状,不要让液体建模溢出三角烧瓶。
* **注意:** 三角烧瓶的建模整个模型的方向朝向向上。
**"大象牙膏"喷发效果模拟:**
1. **触发:** 页面在footer的位置提供一个按钮点击后开始喷发。
2. **泡沫形态与质感:**
* 喷射出的泡沫应由**大量微小、半透明、能够相互融合的粒子**组成,模拟真实泡沫的**稠密和蓬松质感**,避免看起来像孤立的小球。颜色为荧光粉色,可以带有一些亮度变化以增加层次感。
* 考虑使用一个简单的**噪点纹理**或程序化方式为泡沫粒子增加一些表面细节,使其看起来更像多孔的泡沫而非光滑球体。
3. **喷射动态与流体模拟:**
* **初期喷发:** 泡沫从烧瓶口猛烈向上喷出,形成一个持续上升的圆柱(紧密的泡沫堆积在一起形成柱状)。高度至少要有三角烧瓶的3倍高度,泡沫住直到快达到最大高度的时候才开始分散,初始喷射速度和压力较大。
* **压力衰减:** 喷射的强度(速度和产生泡沫的速率)应随时间**逐渐减弱**,导致泡沫柱的高度和喷射距离逐渐减小,模拟化学反应物耗尽的过程。
* **粒子运动:** 粒子运动轨迹应模拟**基本的流体动力学影响**,而不仅仅是简单的抛物线。例如,粒子之间应有轻微的排斥力以模拟泡沫的膨胀感,或者在喷射主流周围有随机的速度扰动。整体受重力影响。
4. **泡沫与环境交互及变形:**
* **重力与堆积:** 喷出的泡沫粒子受重力影响下落。当泡沫颗粒接触平面或烧瓶外壁时,它们应该**模拟受压变形的效果**,例如在垂直方向上被压扁(减少Y轴缩放),并在水平方向上略微扩展(增加X和Z轴缩放)。
* **堆积形态:** 落在平面和烧瓶上的泡沫应该能逐渐**堆积起来,形成一定厚度的覆盖层**,而不是消失。堆积的泡沫也应有类似的变形和融合效果。
* **泡沫固定: ** 泡沫在落到物体表面后,略微滑行一段距离便停止移动。
5. **液体减少:**
随着喷发,三角烧瓶内的液体液面逐渐下降,用来演示液体减少的效果。注意不是液体整体缩小,而是液体的顶部的面逐渐下降的效果。
**光照与渲染:**
1. **光源:**
* 设置一个主光源,以产生清晰的阴影。
* 添加一个环境光以提亮场景暗部,确保没有纯黑区域。
2. **阴影:** 启用渲染器的阴影贴图 。平面应能接收阴影,烧瓶和喷出的泡沫应能投射阴影。
3. **反射与折射:** 烧瓶的材质应能展示出环境的微弱反射和光线透过液体及玻璃的折射。
**摄像机与控制:**
* 摄像机应从斜45度角俯视场景中心(三角烧瓶所在位置),确保能清晰观察到整个喷发过程和最终堆积效果。
HTML
Preview
Copy
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>大象牙膏实验 - 3D演示</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
}
#canvas-container {
width: 100vw;
height: 100vh;
}
footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
color: white;
padding: 15px;
text-align: center;
z-index: 100;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
button {
background: linear-gradient(45deg, #ff006e, #8338ec);
color: white;
border: none;
padding: 12px 25px;
font-size: 16px;
font-weight: bold;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(131, 56, 236, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(131, 56, 236, 0.5);
}
button:disabled {
background: linear-gradient(45deg, #6c757d, #495057);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
</style>
</head>
<body>
<div id="canvas-container"></div>
<footer>
<button id="startBtn">开始实验</button>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script>
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a1a);
scene.fog = new THREE.Fog(0x0a0a1a, 50, 300);
// 相机设置
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(60, 40, 60);
camera.lookAt(0, 0, 0);
// 渲染器设置
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.physicallyCorrectLights = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.getElementById('canvas-container').appendChild(renderer.domElement);
// 控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2;
// 光照设置
const ambientLight = new THREE.AmbientLight(0x404040, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(50, 100, 50);
directionalLight.castShadow = true;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 200;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// 添加点光源增强效果
const pointLight = new THREE.PointLight(0xff1493, 0.5, 100);
pointLight.position.set(0, 30, 0);
scene.add(pointLight);
// 创建地面
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x404040,
roughness: 0.1,
metalness: 0.1
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// 创建三角烧瓶
function createFlask() {
const flaskGroup = new THREE.Group();
// 烧瓶形状 - 使用更准确的轮廓
const flaskShape = new THREE.Shape();
const points = [];
// 底部 - 更扁平
points.push(new THREE.Vector2(0, 0));
points.push(new THREE.Vector2(12, 0));
// 主体 - 更宽
for (let i = 0; i <= 15; i++) {
const angle = (i / 15) * Math.PI * 0.7;
const x = 12 + Math.sin(angle) * 8;
const y = i * 1.2;
points.push(new THREE.Vector2(x, y));
}
// 颈部过渡
points.push(new THREE.Vector2(6, 20));
points.push(new THREE.Vector2(3, 22));
points.push(new THREE.Vector2(2.5, 25));
const flaskGeometry = new THREE.LatheGeometry(points, 32);
// 玻璃材质
const flaskMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.9,
roughness: 0.95,
metalness: 0.35,
clearcoat: 1.0,
clearcoatRoughness: 0.03,
transmission: 0.95,
ior: 1.5,
side: THREE.DoubleSide,
thickness: 0.5
});
const flask = new THREE.Mesh(flaskGeometry, flaskMaterial);
flask.castShadow = true;
flask.receiveShadow = true;
flaskGroup.add(flask);
return flaskGroup;
}
const flask = createFlask();
scene.add(flask);
// 创建液体
function createLiquid() {
const liquidGeometry = new THREE.CylinderGeometry(11.5, 11.5, 8, 32);
const liquidMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff1493,
transparent: true,
opacity: 0.8,
roughness: 0.2,
metalness: 0.1,
emissive: 0x330011,
emissiveIntensity: 0.3,
clearcoat: 0.5
});
const liquid = new THREE.Mesh(liquidGeometry, liquidMaterial);
liquid.position.y = 4;
liquid.castShadow = true;
return liquid;
}
const liquid = createLiquid();
scene.add(liquid);
// 泡沫粒子系统 - 包含位置、速度、生命周期等属性
const foamParticles = [];
const maxParticles = 1500;
let isErupting = false;
let eruptionTime = 0;
let liquidLevel = 8;
// 创建泡沫纹理 - 使用程序生成的噪点纹理
function createFoamTexture() {
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 64, 64);
// 添加噪点
for (let i = 0; i < 500; i++) {
const x = Math.random() * 64;
const y = Math.random() * 64;
const size = Math.random() * 3 + 1;
ctx.fillStyle = `rgba(255, 182, 193, ${Math.random() * 0.5})`;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
}
return new THREE.CanvasTexture(canvas);
}
const foamTexture = createFoamTexture();
// 泡沫材质
const foamMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff1493,
transparent: true,
opacity: 0.7,
roughness: 0.8,
metalness: 0,
emissive: 0x440022,
emissiveIntensity: 0.2,
map: foamTexture
});
// 创建泡沫粒子
function createFoamParticle() {
const size = Math.random() * 1.2 + 0.6;
const geometry = new THREE.SphereGeometry(size, 8, 8);
const particle = new THREE.Mesh(geometry, foamMaterial.clone());
particle.castShadow = true;
particle.receiveShadow = true;
return {
mesh: particle,
velocity: new THREE.Vector3(),
life: 0,
maxLife: Math.random() * 400 + 200,
originalSize: size,
deformation: new THREE.Vector3(1, 1, 1)
};
}
// 初始化粒子池
for (let i = 0; i < maxParticles; i++) {
const particle = createFoamParticle();
foamParticles.push(particle);
}
// 喷发函数 - 处理泡沫粒子的生成、初始速度和位置
function erupt() {
if (!isErupting) return;
const currentTime = Date.now();
const elapsed = (currentTime - eruptionTime) / 1000;
// 喷发强度随时间衰减
const intensity = Math.max(0, 1 - elapsed / 15);
if (intensity <= 0) {
isErupting = false;
return;
}
// 每帧生成新粒子
const particlesPerFrame = Math.floor(intensity * 8);
for (let i = 0; i < particlesPerFrame; i++) {
const particle = foamParticles.find(p => p.life <= 0);
if (!particle) continue;
// 重置粒子
particle.mesh.position.set(
(Math.random() - 0.5) * 2,
25,
(Math.random() - 0.5) * 2
);
particle.mesh.scale.set(1, 1, 1);
particle.mesh.material.opacity = 0.7;
particle.mesh.visible = true;
// 设置初始速度 - 向上喷发
const angle = Math.random() * Math.PI * 2;
const speed = intensity * (25 + Math.random() * 10);
const horizontalSpeed = 2 + Math.random() * 3;
particle.velocity.set(
Math.cos(angle) * horizontalSpeed,
speed,
Math.sin(angle) * horizontalSpeed
);
particle.life = 1;
particle.maxLife = Math.random() * 400 + 200;
particle.deformation.set(1, 1, 1);
scene.add(particle.mesh);
}
// 减少液体
if (liquidLevel > 0 && Math.random() < 0.1) {
liquidLevel -= 0.03;
liquid.scale.y = Math.max(0.1, liquidLevel / 8);
liquid.position.y = Math.max(0.5, liquidLevel / 2);
}
}
// 更新泡沫粒子 - 包含物理模拟和生命周期管理
function updateFoamParticles() {
foamParticles.forEach(particle => {
if (particle.life <= 0) return;
const deltaTime = 0.016;
// 重力和空气阻力
particle.velocity.y -= 12 * deltaTime;
particle.velocity.multiplyScalar(0.995);
// 更新位置
particle.mesh.position.add(
particle.velocity.clone().multiplyScalar(deltaTime)
);
// 碰撞检测 - 地面
if (particle.mesh.position.y <= 0.5) {
particle.mesh.position.y = 0.5;
// 变形效果
const compression = Math.min(0.7, Math.abs(particle.velocity.y) * 0.05);
particle.deformation.y = Math.max(0.3, 1 - compression);
particle.deformation.x = Math.min(1.5, 1 + compression * 0.5);
particle.deformation.z = Math.min(1.5, 1 + compression * 0.5);
particle.velocity.y *= -0.1;
particle.velocity.x *= 0.8;
particle.velocity.z *= 0.8;
}
// 碰撞检测 - 烧瓶
const distanceFromCenter = Math.sqrt(
particle.mesh.position.x ** 2 + particle.mesh.position.z ** 2
);
if (distanceFromCenter < 15 && particle.mesh.position.y < 20) {
// 反弹和变形
const normal = new THREE.Vector3(
particle.mesh.position.x / distanceFromCenter,
0,
particle.mesh.position.z / distanceFromCenter
);
particle.velocity.reflect(normal);
particle.velocity.multiplyScalar(0.5);
}
// 应用变形
particle.mesh.scale.copy(particle.deformation);
// 生命周期
particle.life++;
const lifeRatio = particle.life / particle.maxLife;
// 淡出效果
if (lifeRatio > 0.7) {
particle.mesh.material.opacity = 0.7 * (1 - (lifeRatio - 0.7) / 0.3);
}
// 移除死亡粒子
if (particle.life >= particle.maxLife) {
scene.remove(particle.mesh);
particle.life = 0;
}
});
}
// 开始实验
document.getElementById('startBtn').addEventListener('click', function() {
if (!isErupting) {
isErupting = true;
eruptionTime = Date.now();
this.disabled = true;
this.textContent = '实验进行中...';
// 10秒后重新启用按钮
setTimeout(() => {
this.disabled = false;
this.textContent = '重新开始';
}, 10000);
}
});
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
// 更新喷发
erupt();
// 更新泡沫粒子
updateFoamParticles();
// 液体表面波动
if (liquidLevel > 0) {
liquid.rotation.y += 0.005;
liquid.position.y = Math.max(0.5, liquidLevel / 2) + Math.sin(Date.now() * 0.002) * 0.1;
}
renderer.render(scene, camera);
}
// 窗口大小调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 开始动画
animate();
</script>
</body>
</html>