Tingchenliang commited on
Commit
644d396
·
verified ·
1 Parent(s): b4c0b45

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +530 -19
index.html CHANGED
@@ -1,19 +1,530 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Three.js 云霄飞车动画</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <style>
8
+ html, body {
9
+ margin: 0;
10
+ padding: 0;
11
+ overflow: hidden;
12
+ background: #ffeef7; /* 淡粉色背景(浏览器无WebGL时可见) */
13
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
14
+ }
15
+ #info {
16
+ position: absolute;
17
+ top: 10px;
18
+ left: 10px;
19
+ color: #333;
20
+ background: rgba(255, 255, 255, 0.85);
21
+ padding: 8px 12px;
22
+ border-radius: 8px;
23
+ font-size: 12px;
24
+ z-index: 10;
25
+ user-select: none;
26
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
27
+ }
28
+ #info strong { color: #d23; }
29
+ #info a {
30
+ color: #06c;
31
+ text-decoration: none;
32
+ border-bottom: 1px dashed #06c;
33
+ }
34
+ #info a:hover { color: #028; }
35
+ </style>
36
+ </head>
37
+ <body>
38
+ <div id="info">
39
+ <div><strong>Three.js 云霄飞车</strong></div>
40
+ <div>• 视角模式: 1 第一人称 | 2 第三人称 | R 重置视角</div>
41
+ <div>• 鼠标滚轮缩放,右键/中键平移(左键旋转,模式2)</div>
42
+ <div>• 自动循环播放</div>
43
+ <div style="margin-top:4px">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener">anycoder</a></div>
44
+ </div>
45
+
46
+ <!-- Three.js 和 OrbitControls(全局变量) -->
47
+ <script src="https://unpkg.com/[email protected]/build/three.min.js"></script>
48
+ <script src="https://unpkg.com/[email protected]/examples/js/controls/OrbitControls.js"></script>
49
+
50
+ <script>
51
+ // 核心全局
52
+ let scene, renderer, camera, orbit;
53
+ let rideCam, followCam;
54
+ let coasterCurve, tubeMesh, ball;
55
+ let clock = new THREE.Clock();
56
+ let tParam = 0; // 0..1 路径参数
57
+ let viewMode = 1; // 1: 第一人称, 2: 第三人称
58
+ const TUBULAR_SEGMENTS = 1400;
59
+ const RADIAL_SEGMENTS = 16;
60
+ const TRACK_RADIUS = 0.8;
61
+
62
+ // 初始化
63
+ init();
64
+ animate();
65
+
66
+ function init() {
67
+ // 场景
68
+ scene = new THREE.Scene();
69
+ scene.background = new THREE.Color(0xffeef7); // 淡粉色
70
+
71
+ // 渲染器
72
+ renderer = new THREE.WebGLRenderer({ antialias: true });
73
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
74
+ renderer.setSize(window.innerWidth, window.innerHeight);
75
+ renderer.shadowMap.enabled = true;
76
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
77
+ document.body.appendChild(renderer.domElement);
78
+
79
+ // 相机
80
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
81
+ camera.position.set(50, 40, 60);
82
+
83
+ // OrbitControls(第三人称使用)
84
+ orbit = new THREE.OrbitControls(camera, renderer.domElement);
85
+ orbit.enableDamping = true;
86
+ orbit.dampingFactor = 0.08;
87
+
88
+ // 光照
89
+ const ambient = new THREE.AmbientLight(0xffffff, 0.45);
90
+ scene.add(ambient);
91
+
92
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
93
+ dirLight.position.set(80, 100, 40);
94
+ dirLight.castShadow = true;
95
+ dirLight.shadow.mapSize.set(2048, 2048);
96
+ dirLight.shadow.camera.near = 1;
97
+ dirLight.shadow.camera.far = 300;
98
+ dirLight.shadow.camera.left = -120;
99
+ dirLight.shadow.camera.right = 120;
100
+ dirLight.shadow.camera.top = 120;
101
+ dirLight.shadow.camera.bottom = -120;
102
+ scene.add(dirLight);
103
+
104
+ // 地面(灰白色)
105
+ const ground = new THREE.Mesh(
106
+ new THREE.PlaneGeometry(400, 400),
107
+ new THREE.MeshStandardMaterial({ color: 0xf3f3f3, roughness: 0.95, metalness: 0.0 })
108
+ );
109
+ ground.rotation.x = -Math.PI / 2;
110
+ ground.receiveShadow = true;
111
+ scene.add(ground);
112
+
113
+ // 构建轨道
114
+ buildCoaster();
115
+
116
+ // 小球
117
+ const ballGeo = new THREE.SphereGeometry(0.7, 32, 16);
118
+ const ballMat = new THREE.MeshStandardMaterial({ color: 0xff5555, roughness: 0.35, metalness: 0.2 });
119
+ ball = new THREE.Mesh(ballGeo, ballMat);
120
+ ball.castShadow = true;
121
+ ball.position.copy(coasterCurve.getPointAt(0));
122
+ scene.add(ball);
123
+
124
+ // 相机 - 第一人称(rideCam)
125
+ rideCam = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.05, 2000);
126
+ // 初始放在小球后方
127
+ const initForward = coasterCurve.getTangentAt(0);
128
+ const up = new THREE.Vector3(0, 1, 0);
129
+ const side = new THREE.Vector3().crossVectors(up, initForward).normalize();
130
+ const upOrth = new THREE.Vector3().crossVectors(initForward, side).normalize();
131
+ const behind = initForward.clone().multiplyScalar(-2.5).add(upOrth.clone().multiplyScalar(0.6));
132
+ rideCam.position.copy(ball.position.clone().add(behind));
133
+ scene.add(rideCam);
134
+
135
+ // 相机 - 第三人称(followCam,使用主相机)
136
+ followCam = camera;
137
+
138
+ // 事件
139
+ window.addEventListener('resize', onResize, false);
140
+ window.addEventListener('keydown', onKeyDown, false);
141
+ }
142
+
143
+ function onResize() {
144
+ const w = window.innerWidth;
145
+ const h = window.innerHeight;
146
+ renderer.setSize(w, h);
147
+ camera.aspect = w / h;
148
+ camera.updateProjectionMatrix();
149
+
150
+ rideCam.aspect = w / h;
151
+ rideCam.updateProjectionMatrix();
152
+ }
153
+
154
+ function onKeyDown(e) {
155
+ if (e.key === '1') {
156
+ viewMode = 1;
157
+ } else if (e.key === '2') {
158
+ viewMode = 2;
159
+ } else if (e.key.toLowerCase() === 'r') {
160
+ // 重置相机
161
+ camera.position.set(50, 40, 60);
162
+ camera.lookAt(0, 10, 0);
163
+ orbit.target.set(0, 10, 0);
164
+ orbit.update();
165
+ }
166
+ }
167
+
168
+ // 构建复杂轨道(多段组合 + 过渡 + 平滑)
169
+ function buildCoaster() {
170
+ const pts = [];
171
+
172
+ // 起点
173
+ const start = new THREE.Vector3(0, 0, -30);
174
+ pts.push(start);
175
+
176
+ // 辅助函数
177
+ const addPoints = (arr) => { for (const p of arr) pts.push(p.clone()); };
178
+ const normalizeToUnit = (v) => v.lengthSq() < 1e-6 ? new THREE.Vector3(1,0,0) : v.clone().normalize();
179
+
180
+ // 段1: 初始缓坡(平滑过渡到螺旋)
181
+ const rampLen = 12;
182
+ const rampPts = [];
183
+ for (let i = 0; i <= rampLen; i++) {
184
+ const t = i / rampLen;
185
+ const z = THREE.MathUtils.lerp(-30, -28, t);
186
+ const y = THREE.MathUtils.lerp(0, 2, t); // 缓坡上升
187
+ rampPts.push(new THREE.Vector3(0, y, z));
188
+ }
189
+ addPoints(rampPts);
190
+
191
+ // 段2: 螺旋上升(绕Y轴旋转)
192
+ const helix2 = generateHelixPoints({
193
+ startPoint: rampPts[rampPts.length - 1],
194
+ turns: 3,
195
+ height: 24,
196
+ radiusStart: 2,
197
+ radiusEnd: 10,
198
+ direction: +1, // 上升
199
+ verticalOffset: +4,
200
+ samplesPerTurn: 80
201
+ });
202
+ addPoints(helix2);
203
+
204
+ // 段3: 垂直环(Loop-the-Loop)
205
+ const afterHelix2 = helix2[helix2.length - 1];
206
+ const loopRadius = 10;
207
+ const loopCenter = afterHelix2.clone().add(new THREE.Vector3(0, loopRadius, 0)); // 环底在当前点
208
+ const loopStart = afterHelix2.clone();
209
+ const loopUpDir = normalizeToUnit(loopStart.clone().sub(loopCenter));
210
+ const loopRightDir = new THREE.Vector3().crossVectors(loopUpDir, new THREE.Vector3(0,1,0)).normalize();
211
+ const loop = generateVerticalLoopPoints({
212
+ center: loopCenter,
213
+ radius: loopRadius,
214
+ startDirection: loopUpDir, // 从底部向上
215
+ samples: 140
216
+ });
217
+ addPoints(loop);
218
+
219
+ // 段4: 波浪/S 形横向穿梭(多层交叉感)
220
+ const afterLoop = loop[loop.length - 1];
221
+ const wave = generateWaveCrossoverPoints({
222
+ startPoint: afterLoop,
223
+ length: 70,
224
+ waves: 2,
225
+ amplitudeX: 16,
226
+ amplitudeZ: 8,
227
+ deltaY: 8,
228
+ verticalDir: +1, // 渐升
229
+ samples: 200
230
+ });
231
+ addPoints(wave);
232
+
233
+ // 段5: 漏斗式俯冲(半径缩小 + 高度下降 + 半圈旋转)
234
+ const afterWave = wave[wave.length - 1];
235
+ const funnel = generateFunnelPoints({
236
+ startPoint: afterWave,
237
+ turns: 1.6,
238
+ heightDrop: 20,
239
+ radiusStart: 10,
240
+ radiusEnd: 2,
241
+ direction: -1, // 下降
242
+ samplesPerTurn: 100
243
+ });
244
+ addPoints(funnel);
245
+
246
+ // 段6: 抛射与回归(抛物线段)
247
+ const afterFunnel = funnel[funnel.length - 1];
248
+ const launch = generateLaunchAndReturnPoints({
249
+ startPoint: afterFunnel,
250
+ length: 40,
251
+ height: 22,
252
+ samples: 90
253
+ });
254
+ addPoints(launch);
255
+
256
+ // 段7: 缓坡与过渡,回到起点附近
257
+ const afterLaunch = launch[launch.length - 1];
258
+ const backToStart = [];
259
+ const lastToFirst = afterLaunch.clone().lerp(start, 1);
260
+ // 逐渐回到起点同高同位置
261
+ for (let i = 0; i <= 40; i++) {
262
+ const t = i / 40;
263
+ const p = new THREE.Vector3().lerpVectors(afterLaunch, start, t);
264
+ // 轻微抬高过渡,避免锐角
265
+ p.y = THREE.MathUtils.lerp(afterLaunch.y, start.y, t) + Math.sin(t * Math.PI) * 0.8;
266
+ backToStart.push(p);
267
+ }
268
+ addPoints(backToStart);
269
+
270
+ // 点平滑(Chaikin)
271
+ const smoothed = chaikinSmooth(pts, 2);
272
+
273
+ // 构建闭合曲线
274
+ coasterCurve = new THREE.CatmullRomCurve3(smoothed, true, 'chordal', 0.12);
275
+
276
+ // 轨道管道
277
+ const tubeGeo = new THREE.TubeGeometry(coasterCurve, TUBULAR_SEGMENTS, TRACK_RADIUS, RADIAL_SEGMENTS, true);
278
+ const tubeMat = new THREE.MeshStandardMaterial({
279
+ color: 0xffffff,
280
+ roughness: 0.25,
281
+ metalness: 0.0,
282
+ transparent: true,
283
+ opacity: 0.92,
284
+ envMapIntensity: 0.3
285
+ });
286
+ tubeMesh = new THREE.Mesh(tubeGeo, tubeMat);
287
+ tubeMesh.castShadow = false;
288
+ tubeMesh.receiveShadow = true;
289
+ scene.add(tubeMesh);
290
+
291
+ // 支撑系统
292
+ addSupportPosts(coasterCurve, {
293
+ every: 0.004, // 约每0.4%路径一个支柱(密度可调)
294
+ minHeight: 10, // 仅当轨道离地高度大于该值时放置
295
+ poleRadius: 0.18,
296
+ poleColor: 0x9e9e9e
297
+ });
298
+
299
+ // 轨道装饰标记(在部分关键点放置小标记)
300
+ addTrackMarkers(coasterCurve, 60, 0x3366ff);
301
+ }
302
+
303
+ // 生成螺旋段(可改变半径和高度)
304
+ function generateHelixPoints({
305
+ startPoint,
306
+ turns = 2,
307
+ height = 10,
308
+ radiusStart = 5,
309
+ radiusEnd = 5,
310
+ direction = +1, // +1 up, -1 down
311
+ verticalOffset = 0, // 额外Y偏移
312
+ samplesPerTurn = 60
313
+ }) {
314
+ const pts = [];
315
+ const totalSamples = Math.max(20, Math.floor(turns * samplesPerTurn));
316
+ for (let i = 0; i <= totalSamples; i++) {
317
+ const t = i / totalSamples;
318
+ const angle = t * turns * Math.PI * 2 * direction;
319
+ const r = THREE.MathUtils.lerp(radiusStart, radiusEnd, t);
320
+ const x = startPoint.x + r * Math.cos(angle);
321
+ const z = startPoint.z + r * Math.sin(angle);
322
+ const y = startPoint.y + (t * height * direction) + verticalOffset;
323
+ pts.push(new THREE.Vector3(x, y, z));
324
+ }
325
+ return pts;
326
+ }
327
+
328
+ // 生成垂直环(Loop-the-Loop)
329
+ function generateVerticalLoopPoints({
330
+ center,
331
+ radius,
332
+ startDirection, // 环底处的切线方向(向上)
333
+ samples = 80
334
+ }) {
335
+ const pts = [];
336
+ // 构建正交基
337
+ const up = startDirection.clone().normalize(); // 切向
338
+ let side = new THREE.Vector3().crossVectors(up, new THREE.Vector3(0,0,1));
339
+ if (side.lengthSq() < 1e-6) side = new THREE.Vector3().crossVectors(up, new THREE.Vector3(1,0,0));
340
+ side.normalize(); // 横向
341
+ const forward = new THREE.Vector3().crossVectors(side, up).normalize(); // 指向环内部
342
+
343
+ // 从环底开始(-90°)到 270°(等价 -90°),完整一圈
344
+ for (let i = 0; i <= samples; i++) {
345
+ const t = i / samples;
346
+ const theta = -Math.PI / 2 + t * Math.PI * 2;
347
+ const offset = up.clone().multiplyScalar(radius * Math.sin(theta))
348
+ .add(side.clone().multiplyScalar(radius * Math.cos(theta)));
349
+ const p = center.clone().add(offset);
350
+ pts.push(p);
351
+ }
352
+ return pts;
353
+ }
354
+
355
+ // 生成波浪/横向S形穿梭段(模拟多层交叉)
356
+ function generateWaveCrossoverPoints({
357
+ startPoint,
358
+ length = 50,
359
+ waves = 2,
360
+ amplitudeX = 12,
361
+ amplitudeZ = 6,
362
+ deltaY = 6,
363
+ verticalDir = +1, // 向上为+
364
+ samples = 120
365
+ }) {
366
+ const pts = [];
367
+ for (let i = 0; i <= samples; i++) {
368
+ const t = i / samples;
369
+ const s = t * waves * Math.PI * 2; // 多个S周期
370
+ const x = startPoint.x + Math.sin(s) * amplitudeX;
371
+ const z = startPoint.z + Math.cos(s) * amplitudeZ;
372
+ const y = startPoint.y + t * deltaY * verticalDir;
373
+ pts.push(new THREE.Vector3(x, y, z));
374
+ }
375
+ return pts;
376
+ }
377
+
378
+ // 生成漏斗段(半径缩小 + 下降 + 半圈旋转)
379
+ function generateFunnelPoints({
380
+ startPoint,
381
+ turns = 1.5,
382
+ heightDrop = 12,
383
+ radiusStart = 8,
384
+ radiusEnd = 2,
385
+ direction = -1, // 下降
386
+ samplesPerTurn = 80
387
+ }) {
388
+ const totalSamples = Math.max(20, Math.floor(turns * samplesPerTurn));
389
+ const pts = [];
390
+ for (let i = 0; i <= totalSamples; i++) {
391
+ const t = i / totalSamples;
392
+ const angle = t * turns * Math.PI * 2 * direction;
393
+ const r = THREE.MathUtils.lerp(radiusStart, radiusEnd, t);
394
+ const x = startPoint.x + r * Math.cos(angle);
395
+ const z = startPoint.z + r * Math.sin(angle);
396
+ const y = startPoint.y - t * heightDrop * (direction === -1 ? 1 : -1);
397
+ pts.push(new THREE.Vector3(x, y, z));
398
+ }
399
+ return pts;
400
+ }
401
+
402
+ // 生成抛射与回归段(抛物线)
403
+ function generateLaunchAndReturnPoints({
404
+ startPoint,
405
+ length = 30,
406
+ height = 15,
407
+ samples = 60
408
+ }) {
409
+ const pts = [];
410
+ const axis = new THREE.Vector3(1, 0, 0); // 沿X方向
411
+ for (let i = 0; i <= samples; i++) {
412
+ const t = i / samples;
413
+ const s = t * length;
414
+ // 抛物线:y = -4h/L^2 * s^2 + 4h/L * s
415
+ const y = -4 * height / (length * length) * s * s + 4 * height / length * s;
416
+ const p = startPoint.clone().add(axis.clone().multiplyScalar(s));
417
+ p.y += y;
418
+ pts.push(p);
419
+ }
420
+ return pts;
421
+ }
422
+
423
+ // Chaikin 曲线平滑算法(1-2次迭代足够)
424
+ function chaikinSmooth(points, iterations = 1) {
425
+ let pts = points.map(p => p.clone());
426
+ for (let it = 0; it < iterations; it++) {
427
+ const newPts = [];
428
+ newPts.push(pts[0].clone());
429
+ for (let i = 0; i < pts.length - 1; i++) {
430
+ const p = pts[i];
431
+ const q = pts[i + 1];
432
+ const Q = p.clone().multiplyScalar(0.75).add(q.clone().multiplyScalar(0.25));
433
+ const R = p.clone().multiplyScalar(0.25).add(q.clone().multiplyScalar(0.75));
434
+ newPts.push(Q, R);
435
+ }
436
+ newPts.push(pts[pts.length - 1].clone());
437
+ pts = newPts;
438
+ }
439
+ return pts;
440
+ }
441
+
442
+ // 添加支撑柱
443
+ function addSupportPosts(curve, {
444
+ every = 0.01, // 0..1 间距
445
+ minHeight = 10, // 最小离地高度
446
+ poleRadius = 0.15,
447
+ poleColor = 0x999999
448
+ } = {}) {
449
+ const groundY = 0;
450
+ const poleMat = new THREE.MeshStandardMaterial({ color: poleColor, roughness: 0.5, metalness: 0.2 });
451
+ for (let t = 0; t <= 1; t += every) {
452
+ const p = curve.getPointAt(t);
453
+ const height = p.y - groundY - TRACK_RADIUS;
454
+ if (height < minHeight) continue;
455
+
456
+ const poleHeight = Math.max(0.5, height);
457
+ const poleGeo = new THREE.CylinderGeometry(poleRadius, poleRadius, poleHeight, 12);
458
+ const pole = new THREE.Mesh(poleGeo, poleMat);
459
+ pole.castShadow = true;
460
+ pole.receiveShadow = true;
461
+ pole.position.set(p.x, groundY + poleHeight / 2, p.z);
462
+ scene.add(pole);
463
+
464
+ // 顶部连接件(装饰)
465
+ const capGeo = new THREE.CylinderGeometry(poleRadius * 1.4, poleRadius * 1.4, 0.2, 12);
466
+ const cap = new THREE.Mesh(capGeo, poleMat);
467
+ cap.position.set(p.x, groundY + poleHeight + 0.1, p.z);
468
+ cap.castShadow = true;
469
+ scene.add(cap);
470
+
471
+ // 底部底座(装饰)
472
+ const baseGeo = new THREE.CylinderGeometry(poleRadius * 1.8, poleRadius * 1.8, 0.15, 12);
473
+ const base = new THREE.Mesh(baseGeo, poleMat);
474
+ base.position.set(p.x, groundY + 0.075, p.z);
475
+ base.receiveShadow = true;
476
+ scene.add(base);
477
+ }
478
+ }
479
+
480
+ // 轨道关键点标记(可调试用)
481
+ function addTrackMarkers(curve, count = 40, color = 0x3366ff) {
482
+ const markerGeo = new THREE.SphereGeometry(0.25, 12, 8);
483
+ const markerMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.8 });
484
+ for (let i = 0; i < count; i++) {
485
+ const t = i / count;
486
+ const p = curve.getPointAt(t);
487
+ const m = new THREE.Mesh(markerGeo, markerMat);
488
+ m.position.copy(p);
489
+ scene.add(m);
490
+ }
491
+ }
492
+
493
+ // 动画循环
494
+ function animate() {
495
+ requestAnimationFrame(animate);
496
+ const dt = clock.getDelta();
497
+ const speed = 0.02; // 路径速度(越大越快)
498
+
499
+ tParam = (tParam + speed * dt) % 1.0;
500
+
501
+ // 更新小球位置与朝向
502
+ const pos = coasterCurve.getPointAt(tParam);
503
+ const tan = coasterCurve.getTangentAt(tParam);
504
+ ball.position.copy(pos);
505
+ // 让小球看起来沿着切线方向(可选)
506
+ const lookTarget = pos.clone().add(tan);
507
+ ball.lookAt(lookTarget);
508
+
509
+ // 相机更新
510
+ if (viewMode === 1) {
511
+ // 第一人称:rideCam 跟随小球(稍上方后方)
512
+ const up = new THREE.Vector3(0, 1, 0);
513
+ const side = new THREE.Vector3().crossVectors(up, tan).normalize();
514
+ const upOrth = new THREE.Vector3().crossVectors(tan, side).normalize();
515
+ const camOffset = tan.clone().multiplyScalar(-2.2).add(upOrth.clone().multiplyScalar(0.7));
516
+ rideCam.position.copy(pos.clone().add(camOffset));
517
+ const forwardTarget = pos.clone().add(tan.clone().multiplyScalar(10));
518
+ rideCam.lookAt(forwardTarget);
519
+ } else {
520
+ // 第三人称:OrbitControls(不影响rideCam位置)
521
+ orbit.update();
522
+ }
523
+
524
+ // 渲染
525
+ const activeCam = viewMode === 1 ? rideCam : camera;
526
+ renderer.render(scene, activeCam);
527
+ }
528
+ </script>
529
+ </body>
530
+ </html>