app-rmzeei-66 / index.html
Tingchenliang's picture
Upload index.html with huggingface_hub
644d396 verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>Three.js 云霄飞车动画</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: #ffeef7; /* 淡粉色背景(浏览器无WebGL时可见) */
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: #333;
background: rgba(255, 255, 255, 0.85);
padding: 8px 12px;
border-radius: 8px;
font-size: 12px;
z-index: 10;
user-select: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
#info strong { color: #d23; }
#info a {
color: #06c;
text-decoration: none;
border-bottom: 1px dashed #06c;
}
#info a:hover { color: #028; }
</style>
</head>
<body>
<div id="info">
<div><strong>Three.js 云霄飞车</strong></div>
<div>• 视角模式: 1 第一人称 | 2 第三人称 | R 重置视角</div>
<div>• 鼠标滚轮缩放,右键/中键平移(左键旋转,模式2)</div>
<div>• 自动循环播放</div>
<div style="margin-top:4px">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener">anycoder</a></div>
</div>
<!-- Three.js 和 OrbitControls(全局变量) -->
<script src="https://unpkg.com/three@0.158.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.158.0/examples/js/controls/OrbitControls.js"></script>
<script>
// 核心全局
let scene, renderer, camera, orbit;
let rideCam, followCam;
let coasterCurve, tubeMesh, ball;
let clock = new THREE.Clock();
let tParam = 0; // 0..1 路径参数
let viewMode = 1; // 1: 第一人称, 2: 第三人称
const TUBULAR_SEGMENTS = 1400;
const RADIAL_SEGMENTS = 16;
const TRACK_RADIUS = 0.8;
// 初始化
init();
animate();
function init() {
// 场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0xffeef7); // 淡粉色
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 相机
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(50, 40, 60);
// OrbitControls(第三人称使用)
orbit = new THREE.OrbitControls(camera, renderer.domElement);
orbit.enableDamping = true;
orbit.dampingFactor = 0.08;
// 光照
const ambient = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
dirLight.position.set(80, 100, 40);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048);
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 300;
dirLight.shadow.camera.left = -120;
dirLight.shadow.camera.right = 120;
dirLight.shadow.camera.top = 120;
dirLight.shadow.camera.bottom = -120;
scene.add(dirLight);
// 地面(灰白色)
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(400, 400),
new THREE.MeshStandardMaterial({ color: 0xf3f3f3, roughness: 0.95, metalness: 0.0 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// 构建轨道
buildCoaster();
// 小球
const ballGeo = new THREE.SphereGeometry(0.7, 32, 16);
const ballMat = new THREE.MeshStandardMaterial({ color: 0xff5555, roughness: 0.35, metalness: 0.2 });
ball = new THREE.Mesh(ballGeo, ballMat);
ball.castShadow = true;
ball.position.copy(coasterCurve.getPointAt(0));
scene.add(ball);
// 相机 - 第一人称(rideCam)
rideCam = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.05, 2000);
// 初始放在小球后方
const initForward = coasterCurve.getTangentAt(0);
const up = new THREE.Vector3(0, 1, 0);
const side = new THREE.Vector3().crossVectors(up, initForward).normalize();
const upOrth = new THREE.Vector3().crossVectors(initForward, side).normalize();
const behind = initForward.clone().multiplyScalar(-2.5).add(upOrth.clone().multiplyScalar(0.6));
rideCam.position.copy(ball.position.clone().add(behind));
scene.add(rideCam);
// 相机 - 第三人称(followCam,使用主相机)
followCam = camera;
// 事件
window.addEventListener('resize', onResize, false);
window.addEventListener('keydown', onKeyDown, false);
}
function onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
rideCam.aspect = w / h;
rideCam.updateProjectionMatrix();
}
function onKeyDown(e) {
if (e.key === '1') {
viewMode = 1;
} else if (e.key === '2') {
viewMode = 2;
} else if (e.key.toLowerCase() === 'r') {
// 重置相机
camera.position.set(50, 40, 60);
camera.lookAt(0, 10, 0);
orbit.target.set(0, 10, 0);
orbit.update();
}
}
// 构建复杂轨道(多段组合 + 过渡 + 平滑)
function buildCoaster() {
const pts = [];
// 起点
const start = new THREE.Vector3(0, 0, -30);
pts.push(start);
// 辅助函数
const addPoints = (arr) => { for (const p of arr) pts.push(p.clone()); };
const normalizeToUnit = (v) => v.lengthSq() < 1e-6 ? new THREE.Vector3(1,0,0) : v.clone().normalize();
// 段1: 初始缓坡(平滑过渡到螺旋)
const rampLen = 12;
const rampPts = [];
for (let i = 0; i <= rampLen; i++) {
const t = i / rampLen;
const z = THREE.MathUtils.lerp(-30, -28, t);
const y = THREE.MathUtils.lerp(0, 2, t); // 缓坡上升
rampPts.push(new THREE.Vector3(0, y, z));
}
addPoints(rampPts);
// 段2: 螺旋上升(绕Y轴旋转)
const helix2 = generateHelixPoints({
startPoint: rampPts[rampPts.length - 1],
turns: 3,
height: 24,
radiusStart: 2,
radiusEnd: 10,
direction: +1, // 上升
verticalOffset: +4,
samplesPerTurn: 80
});
addPoints(helix2);
// 段3: 垂直环(Loop-the-Loop)
const afterHelix2 = helix2[helix2.length - 1];
const loopRadius = 10;
const loopCenter = afterHelix2.clone().add(new THREE.Vector3(0, loopRadius, 0)); // 环底在当前点
const loopStart = afterHelix2.clone();
const loopUpDir = normalizeToUnit(loopStart.clone().sub(loopCenter));
const loopRightDir = new THREE.Vector3().crossVectors(loopUpDir, new THREE.Vector3(0,1,0)).normalize();
const loop = generateVerticalLoopPoints({
center: loopCenter,
radius: loopRadius,
startDirection: loopUpDir, // 从底部向上
samples: 140
});
addPoints(loop);
// 段4: 波浪/S 形横向穿梭(多层交叉感)
const afterLoop = loop[loop.length - 1];
const wave = generateWaveCrossoverPoints({
startPoint: afterLoop,
length: 70,
waves: 2,
amplitudeX: 16,
amplitudeZ: 8,
deltaY: 8,
verticalDir: +1, // 渐升
samples: 200
});
addPoints(wave);
// 段5: 漏斗式俯冲(半径缩小 + 高度下降 + 半圈旋转)
const afterWave = wave[wave.length - 1];
const funnel = generateFunnelPoints({
startPoint: afterWave,
turns: 1.6,
heightDrop: 20,
radiusStart: 10,
radiusEnd: 2,
direction: -1, // 下降
samplesPerTurn: 100
});
addPoints(funnel);
// 段6: 抛射与回归(抛物线段)
const afterFunnel = funnel[funnel.length - 1];
const launch = generateLaunchAndReturnPoints({
startPoint: afterFunnel,
length: 40,
height: 22,
samples: 90
});
addPoints(launch);
// 段7: 缓坡与过渡,回到起点附近
const afterLaunch = launch[launch.length - 1];
const backToStart = [];
const lastToFirst = afterLaunch.clone().lerp(start, 1);
// 逐渐回到起点同高同位置
for (let i = 0; i <= 40; i++) {
const t = i / 40;
const p = new THREE.Vector3().lerpVectors(afterLaunch, start, t);
// 轻微抬高过渡,避免锐角
p.y = THREE.MathUtils.lerp(afterLaunch.y, start.y, t) + Math.sin(t * Math.PI) * 0.8;
backToStart.push(p);
}
addPoints(backToStart);
// 点平滑(Chaikin)
const smoothed = chaikinSmooth(pts, 2);
// 构建闭合曲线
coasterCurve = new THREE.CatmullRomCurve3(smoothed, true, 'chordal', 0.12);
// 轨道管道
const tubeGeo = new THREE.TubeGeometry(coasterCurve, TUBULAR_SEGMENTS, TRACK_RADIUS, RADIAL_SEGMENTS, true);
const tubeMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.25,
metalness: 0.0,
transparent: true,
opacity: 0.92,
envMapIntensity: 0.3
});
tubeMesh = new THREE.Mesh(tubeGeo, tubeMat);
tubeMesh.castShadow = false;
tubeMesh.receiveShadow = true;
scene.add(tubeMesh);
// 支撑系统
addSupportPosts(coasterCurve, {
every: 0.004, // 约每0.4%路径一个支柱(密度可调)
minHeight: 10, // 仅当轨道离地高度大于该值时放置
poleRadius: 0.18,
poleColor: 0x9e9e9e
});
// 轨道装饰标记(在部分关键点放置小标记)
addTrackMarkers(coasterCurve, 60, 0x3366ff);
}
// 生成螺旋段(可改变半径和高度)
function generateHelixPoints({
startPoint,
turns = 2,
height = 10,
radiusStart = 5,
radiusEnd = 5,
direction = +1, // +1 up, -1 down
verticalOffset = 0, // 额外Y偏移
samplesPerTurn = 60
}) {
const pts = [];
const totalSamples = Math.max(20, Math.floor(turns * samplesPerTurn));
for (let i = 0; i <= totalSamples; i++) {
const t = i / totalSamples;
const angle = t * turns * Math.PI * 2 * direction;
const r = THREE.MathUtils.lerp(radiusStart, radiusEnd, t);
const x = startPoint.x + r * Math.cos(angle);
const z = startPoint.z + r * Math.sin(angle);
const y = startPoint.y + (t * height * direction) + verticalOffset;
pts.push(new THREE.Vector3(x, y, z));
}
return pts;
}
// 生成垂直环(Loop-the-Loop)
function generateVerticalLoopPoints({
center,
radius,
startDirection, // 环底处的切线方向(向上)
samples = 80
}) {
const pts = [];
// 构建正交基
const up = startDirection.clone().normalize(); // 切向
let side = new THREE.Vector3().crossVectors(up, new THREE.Vector3(0,0,1));
if (side.lengthSq() < 1e-6) side = new THREE.Vector3().crossVectors(up, new THREE.Vector3(1,0,0));
side.normalize(); // 横向
const forward = new THREE.Vector3().crossVectors(side, up).normalize(); // 指向环内部
// 从环底开始(-90°)到 270°(等价 -90°),完整一圈
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const theta = -Math.PI / 2 + t * Math.PI * 2;
const offset = up.clone().multiplyScalar(radius * Math.sin(theta))
.add(side.clone().multiplyScalar(radius * Math.cos(theta)));
const p = center.clone().add(offset);
pts.push(p);
}
return pts;
}
// 生成波浪/横向S形穿梭段(模拟多层交叉)
function generateWaveCrossoverPoints({
startPoint,
length = 50,
waves = 2,
amplitudeX = 12,
amplitudeZ = 6,
deltaY = 6,
verticalDir = +1, // 向上为+
samples = 120
}) {
const pts = [];
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const s = t * waves * Math.PI * 2; // 多个S周期
const x = startPoint.x + Math.sin(s) * amplitudeX;
const z = startPoint.z + Math.cos(s) * amplitudeZ;
const y = startPoint.y + t * deltaY * verticalDir;
pts.push(new THREE.Vector3(x, y, z));
}
return pts;
}
// 生成漏斗段(半径缩小 + 下降 + 半圈旋转)
function generateFunnelPoints({
startPoint,
turns = 1.5,
heightDrop = 12,
radiusStart = 8,
radiusEnd = 2,
direction = -1, // 下降
samplesPerTurn = 80
}) {
const totalSamples = Math.max(20, Math.floor(turns * samplesPerTurn));
const pts = [];
for (let i = 0; i <= totalSamples; i++) {
const t = i / totalSamples;
const angle = t * turns * Math.PI * 2 * direction;
const r = THREE.MathUtils.lerp(radiusStart, radiusEnd, t);
const x = startPoint.x + r * Math.cos(angle);
const z = startPoint.z + r * Math.sin(angle);
const y = startPoint.y - t * heightDrop * (direction === -1 ? 1 : -1);
pts.push(new THREE.Vector3(x, y, z));
}
return pts;
}
// 生成抛射与回归段(抛物线)
function generateLaunchAndReturnPoints({
startPoint,
length = 30,
height = 15,
samples = 60
}) {
const pts = [];
const axis = new THREE.Vector3(1, 0, 0); // 沿X方向
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const s = t * length;
// 抛物线:y = -4h/L^2 * s^2 + 4h/L * s
const y = -4 * height / (length * length) * s * s + 4 * height / length * s;
const p = startPoint.clone().add(axis.clone().multiplyScalar(s));
p.y += y;
pts.push(p);
}
return pts;
}
// Chaikin 曲线平滑算法(1-2次迭代足够)
function chaikinSmooth(points, iterations = 1) {
let pts = points.map(p => p.clone());
for (let it = 0; it < iterations; it++) {
const newPts = [];
newPts.push(pts[0].clone());
for (let i = 0; i < pts.length - 1; i++) {
const p = pts[i];
const q = pts[i + 1];
const Q = p.clone().multiplyScalar(0.75).add(q.clone().multiplyScalar(0.25));
const R = p.clone().multiplyScalar(0.25).add(q.clone().multiplyScalar(0.75));
newPts.push(Q, R);
}
newPts.push(pts[pts.length - 1].clone());
pts = newPts;
}
return pts;
}
// 添加支撑柱
function addSupportPosts(curve, {
every = 0.01, // 0..1 间距
minHeight = 10, // 最小离地高度
poleRadius = 0.15,
poleColor = 0x999999
} = {}) {
const groundY = 0;
const poleMat = new THREE.MeshStandardMaterial({ color: poleColor, roughness: 0.5, metalness: 0.2 });
for (let t = 0; t <= 1; t += every) {
const p = curve.getPointAt(t);
const height = p.y - groundY - TRACK_RADIUS;
if (height < minHeight) continue;
const poleHeight = Math.max(0.5, height);
const poleGeo = new THREE.CylinderGeometry(poleRadius, poleRadius, poleHeight, 12);
const pole = new THREE.Mesh(poleGeo, poleMat);
pole.castShadow = true;
pole.receiveShadow = true;
pole.position.set(p.x, groundY + poleHeight / 2, p.z);
scene.add(pole);
// 顶部连接件(装饰)
const capGeo = new THREE.CylinderGeometry(poleRadius * 1.4, poleRadius * 1.4, 0.2, 12);
const cap = new THREE.Mesh(capGeo, poleMat);
cap.position.set(p.x, groundY + poleHeight + 0.1, p.z);
cap.castShadow = true;
scene.add(cap);
// 底部底座(装饰)
const baseGeo = new THREE.CylinderGeometry(poleRadius * 1.8, poleRadius * 1.8, 0.15, 12);
const base = new THREE.Mesh(baseGeo, poleMat);
base.position.set(p.x, groundY + 0.075, p.z);
base.receiveShadow = true;
scene.add(base);
}
}
// 轨道关键点标记(可调试用)
function addTrackMarkers(curve, count = 40, color = 0x3366ff) {
const markerGeo = new THREE.SphereGeometry(0.25, 12, 8);
const markerMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.8 });
for (let i = 0; i < count; i++) {
const t = i / count;
const p = curve.getPointAt(t);
const m = new THREE.Mesh(markerGeo, markerMat);
m.position.copy(p);
scene.add(m);
}
}
// 动画循环
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta();
const speed = 0.02; // 路径速度(越大越快)
tParam = (tParam + speed * dt) % 1.0;
// 更新小球位置与朝向
const pos = coasterCurve.getPointAt(tParam);
const tan = coasterCurve.getTangentAt(tParam);
ball.position.copy(pos);
// 让小球看起来沿着切线方向(可选)
const lookTarget = pos.clone().add(tan);
ball.lookAt(lookTarget);
// 相机更新
if (viewMode === 1) {
// 第一人称:rideCam 跟随小球(稍上方后方)
const up = new THREE.Vector3(0, 1, 0);
const side = new THREE.Vector3().crossVectors(up, tan).normalize();
const upOrth = new THREE.Vector3().crossVectors(tan, side).normalize();
const camOffset = tan.clone().multiplyScalar(-2.2).add(upOrth.clone().multiplyScalar(0.7));
rideCam.position.copy(pos.clone().add(camOffset));
const forwardTarget = pos.clone().add(tan.clone().multiplyScalar(10));
rideCam.lookAt(forwardTarget);
} else {
// 第三人称:OrbitControls(不影响rideCam位置)
orbit.update();
}
// 渲染
const activeCam = viewMode === 1 ? rideCam : camera;
renderer.render(scene, activeCam);
}
</script>
</body>
</html>