Tingchenliang commited on
Commit
43848b1
·
verified ·
1 Parent(s): e45bc31

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +630 -19
index.html CHANGED
@@ -1,19 +1,630 @@
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-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Three.js 云霄飞车动画</title>
7
+ <meta name="description" content="使用 Three.js 实现的云霄飞车管道轨道动画,小球沿复杂3D轨道运动并循环往复。" />
8
+ <meta name="keywords" content="Three.js,云霄飞车,动画,轨道,3D" />
9
+ <style>
10
+ html, body {
11
+ height: 100%;
12
+ margin: 0;
13
+ overflow: hidden;
14
+ background: #fce4ec; /* 淡粉色背景 */
15
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", "Apple Color Emoji", "Segoe UI Emoji";
16
+ }
17
+ #app {
18
+ width: 100%;
19
+ height: 100%;
20
+ position: relative;
21
+ }
22
+ canvas {
23
+ display: block;
24
+ }
25
+ #ui {
26
+ position: absolute;
27
+ top: 10px;
28
+ left: 10px;
29
+ right: 10px;
30
+ display: flex;
31
+ justify-content: space-between;
32
+ align-items: flex-start;
33
+ pointer-events: none;
34
+ color: #333;
35
+ z-index: 10;
36
+ }
37
+ #hud {
38
+ background: rgba(255,255,255,0.8);
39
+ border-radius: 8px;
40
+ padding: 10px 12px;
41
+ box-shadow: 0 2px 10px rgba(0,0,0,0.15);
42
+ max-width: 320px;
43
+ pointer-events: auto;
44
+ backdrop-filter: blur(4px);
45
+ }
46
+ #hud h1 {
47
+ margin: 0 0 6px 0;
48
+ font-size: 16px;
49
+ font-weight: 700;
50
+ }
51
+ #hud .small {
52
+ font-size: 12px;
53
+ opacity: 0.8;
54
+ margin-bottom: 6px;
55
+ line-height: 1.4;
56
+ }
57
+ #hud .row {
58
+ display: flex;
59
+ gap: 12px;
60
+ align-items: center;
61
+ margin: 4px 0;
62
+ }
63
+ #hud .key {
64
+ display: inline-block;
65
+ padding: 1px 6px;
66
+ background: #eee;
67
+ border: 1px solid #ccc;
68
+ border-radius: 4px;
69
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
70
+ font-size: 11px;
71
+ }
72
+ #stats {
73
+ background: rgba(255,255,255,0.8);
74
+ border-radius: 8px;
75
+ padding: 8px 12px;
76
+ min-width: 160px;
77
+ text-align: right;
78
+ pointer-events: none;
79
+ box-shadow: 0 2px 10px rgba(0,0,0,0.15);
80
+ backdrop-filter: blur(4px);
81
+ font-size: 12px;
82
+ line-height: 1.6;
83
+ }
84
+ #brand {
85
+ position: absolute;
86
+ bottom: 10px;
87
+ left: 10px;
88
+ font-size: 12px;
89
+ color: #555;
90
+ background: rgba(255,255,255,0.75);
91
+ padding: 6px 8px;
92
+ border-radius: 6px;
93
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
94
+ backdrop-filter: blur(4px);
95
+ }
96
+ #brand a {
97
+ color: #0066cc;
98
+ text-decoration: none;
99
+ font-weight: 600;
100
+ }
101
+ #brand a:hover {
102
+ text-decoration: underline;
103
+ }
104
+ #controls {
105
+ position: absolute;
106
+ bottom: 10px;
107
+ right: 10px;
108
+ background: rgba(255,255,255,0.8);
109
+ border-radius: 8px;
110
+ padding: 8px 12px;
111
+ box-shadow: 0 2px 10px rgba(0,0,0,0.15);
112
+ pointer-events: auto;
113
+ backdrop-filter: blur(4px);
114
+ }
115
+ #controls label {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 6px;
119
+ font-size: 12px;
120
+ cursor: pointer;
121
+ }
122
+ input[type="range"] {
123
+ width: 160px;
124
+ }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div id="app">
129
+ <div id="ui">
130
+ <div id="hud">
131
+ <h1>Three.js 云霄飞车</h1>
132
+ <div class="small">
133
+ 视角:<span id="viewMode">第三人称</span><br/>
134
+ 操作:
135
+ <span class="key">C</span> 切换视角,
136
+ <span class="key">鼠标</span> 旋转/缩放,
137
+ <span class="key">空格</span> 暂停/继续
138
+ </div>
139
+ <div class="row">
140
+ 速度:<input id="speed" type="range" min="0.05" max="1.00" step="0.05" value="0.35">
141
+ <span id="speedVal">0.35x</span>
142
+ </div>
143
+ </div>
144
+ <div id="stats">
145
+ 粒子点:<span id="ptCount">-</span><br/>
146
+ 支架数:<span id="supCount">-</span><br/>
147
+ 帧率:<span id="fps">-</span> FPS
148
+ </div>
149
+ </div>
150
+ <div id="brand">Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener">anycoder</a></div>
151
+ <div id="controls">
152
+ <label><input id="toggleTrack" type="checkbox" checked> 显示轨道</label>
153
+ <label><input id="toggleSupports" type="checkbox" checked> 显示支架</label>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Three.js 和 OrbitControls(通过CDN) -->
158
+ <script src="https://unpkg.com/three@0.157.0/build/three.min.js"></script>
159
+ <script src="https://unpkg.com/three@0.157.0/examples/js/controls/OrbitControls.js"></script>
160
+
161
+ <script>
162
+ // ======= 全局变量与基础场景 =======
163
+ let scene, camera, renderer, controls;
164
+ let curve, trackMesh, ball;
165
+ let supportsGroup;
166
+ let firstPerson = false;
167
+ let isPaused = false;
168
+ let clock = new THREE.Clock();
169
+ let elapsed = 0;
170
+ let speedMultiplier = 0.35; // 基础速度倍率
171
+
172
+ const HUD = {
173
+ viewMode: document.getElementById('viewMode'),
174
+ speed: document.getElementById('speed'),
175
+ speedVal: document.getElementById('speedVal'),
176
+ ptCount: document.getElementById('ptCount'),
177
+ supCount: document.getElementById('supCount'),
178
+ fps: document.getElementById('fps'),
179
+ toggleTrack: document.getElementById('toggleTrack'),
180
+ toggleSupports: document.getElementById('toggleSupports')
181
+ };
182
+
183
+ init();
184
+ animate();
185
+
186
+ function init() {
187
+ // 场景与渲染器
188
+ scene = new THREE.Scene();
189
+ scene.background = new THREE.Color(0xfce4ec); // 淡粉色背景
190
+
191
+ renderer = new THREE.WebGLRenderer({ antialias: true });
192
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
193
+ renderer.setSize(window.innerWidth, window.innerHeight);
194
+ renderer.shadowMap.enabled = true;
195
+ document.getElementById('app').appendChild(renderer.domElement);
196
+
197
+ // 相机与控制器
198
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
199
+ camera.position.set(0, 35, 70);
200
+
201
+ controls = new THREE.OrbitControls(camera, renderer.domElement);
202
+ controls.enableDamping = true;
203
+ controls.dampingFactor = 0.08;
204
+ controls.target.set(0, 10, 0);
205
+ controls.maxDistance = 200;
206
+ controls.minDistance = 5;
207
+ controls.maxPolarAngle = Math.PI * 0.95;
208
+
209
+ // 光照
210
+ const ambient = new THREE.AmbientLight(0xffffff, 0.55);
211
+ scene.add(ambient);
212
+
213
+ const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
214
+ dirLight.position.set(-40, 80, 40);
215
+ dirLight.castShadow = true;
216
+ dirLight.shadow.mapSize.set(2048, 2048);
217
+ dirLight.shadow.camera.near = 1;
218
+ dirLight.shadow.camera.far = 200;
219
+ dirLight.shadow.camera.left = -120;
220
+ dirLight.shadow.camera.right = 120;
221
+ dirLight.shadow.camera.top = 120;
222
+ dirLight.shadow.camera.bottom = -120;
223
+ scene.add(dirLight);
224
+
225
+ // 地面
226
+ const ground = new THREE.Mesh(
227
+ new THREE.PlaneGeometry(600, 600),
228
+ new THREE.MeshStandardMaterial({ color: 0xf5f5f5, roughness: 0.95, metalness: 0.0 })
229
+ );
230
+ ground.rotation.x = -Math.PI / 2;
231
+ ground.receiveShadow = true;
232
+ scene.add(ground);
233
+
234
+ // 构建轨道和动画球
235
+ createRollerCoaster();
236
+
237
+ // UI 事件
238
+ HUD.speed.addEventListener('input', () => {
239
+ speedMultiplier = parseFloat(HUD.speed.value);
240
+ HUD.speedVal.textContent = speedMultiplier.toFixed(2) + 'x';
241
+ });
242
+ HUD.toggleTrack.addEventListener('change', () => {
243
+ if (trackMesh) trackMesh.visible = HUD.toggleTrack.checked;
244
+ });
245
+ HUD.toggleSupports.addEventListener('change', () => {
246
+ if (supportsGroup) supportsGroup.visible = HUD.toggleSupports.checked;
247
+ });
248
+
249
+ window.addEventListener('resize', onResize);
250
+ window.addEventListener('keydown', onKeyDown);
251
+ }
252
+
253
+ function onResize() {
254
+ camera.aspect = window.innerWidth / window.innerHeight;
255
+ camera.updateProjectionMatrix();
256
+ renderer.setSize(window.innerWidth, window.innerHeight);
257
+ }
258
+
259
+ function onKeyDown(e) {
260
+ if (e.code === 'KeyC') {
261
+ firstPerson = !firstPerson;
262
+ HUD.viewMode.textContent = firstPerson ? '第一人称' : '第三人称';
263
+ // 切换时重置一下控制器
264
+ controls.enabled = !firstPerson;
265
+ } else if (e.code === 'Space') {
266
+ isPaused = !isPaused;
267
+ }
268
+ }
269
+
270
+ // ======= 平滑算法(Chaikin)用于轨道点平滑 =======
271
+ function chaikinSmoothPoints(points, iterations = 2) {
272
+ let pts = points.slice();
273
+ for (let iter = 0; iter < iterations; iter++) {
274
+ if (pts.length < 3) break;
275
+ const newPts = [];
276
+ for (let i = 0; i < pts.length; i++) {
277
+ const p0 = pts[i];
278
+ const p1 = pts[(i + 1) % pts.length];
279
+ const Q = new THREE.Vector3().copy(p0).lerp(p1, 0.25);
280
+ const R = new THREE.Vector3().copy(p0).lerp(p1, 0.75);
281
+ newPts.push(Q, R);
282
+ }
283
+ pts = newPts;
284
+ }
285
+ return pts;
286
+ }
287
+
288
+ // ======= 轨道段落生成函数 =======
289
+
290
+ // 1) 螺旋上升段(半径从 R1 到 R2,升高 dy,共 turns 圈)
291
+ function generateHelixPoints(start, R1, R2, dy, turns = 3, ptsPerTurn = 60) {
292
+ const totalPts = Math.max(20, Math.floor(turns * ptsPerTurn));
293
+ const pts = [];
294
+ for (let i = 0; i <= totalPts; i++) {
295
+ const t = i / totalPts;
296
+ const angle = t * turns * Math.PI * 2;
297
+ const r = THREE.MathUtils.lerp(R1, R2, t);
298
+ const x = start.x + r * Math.cos(angle);
299
+ const z = start.z + r * Math.sin(angle);
300
+ const y = start.y + dy * t + 0.6 * Math.sin(angle * 3) * (1 - t); // 轻微起伏
301
+ pts.push(new THREE.Vector3(x, y, z));
302
+ }
303
+ return { points: pts, end: pts[pts.length - 1].clone() };
304
+ }
305
+
306
+ // 2) 波浪前进段(在 XZ 前进同时 Y 上下振荡)
307
+ function generateWavePoints(start, length = 120, amplitudeY = 8, frequency = 1.5, ptsPerUnit = 1.5) {
308
+ const totalPts = Math.max(50, Math.floor(length * ptsPerUnit));
309
+ const pts = [];
310
+ for (let i = 0; i <= totalPts; i++) {
311
+ const t = i / totalPts;
312
+ const x = start.x + length * t;
313
+ const y = start.y + amplitudeY * Math.sin(t * Math.PI * 2 * frequency);
314
+ const z = start.z + 15 * Math.sin(t * Math.PI * 2);
315
+ pts.push(new THREE.Vector3(x, y, z));
316
+ }
317
+ return { points: pts, end: pts[pts.length - 1].clone() };
318
+ }
319
+
320
+ // 3) 漏斗下降(螺旋半径快速缩小,同时下降)
321
+ function generateFunnelPoints(start, R1, R2, dy, turns = 2.5, ptsPerTurn = 80) {
322
+ const totalPts = Math.max(30, Math.floor(turns * ptsPerTurn));
323
+ const pts = [];
324
+ for (let i = 0; i <= totalPts; i++) {
325
+ const t = i / totalPts;
326
+ const angle = t * turns * Math.PI * 2;
327
+ const r = THREE.MathUtils.lerp(R1, R2, t);
328
+ const x = start.x + r * Math.cos(angle);
329
+ const z = start.z + r * Math.sin(angle);
330
+ const y = start.y - dy * t + 0.4 * Math.cos(angle * 4) * (1 - t);
331
+ pts.push(new THREE.Vector3(x, y, z));
332
+ }
333
+ return { points: pts, end: pts[pts.length - 1].clone() };
334
+ }
335
+
336
+ // 4) 垂直环(Loop-the-Loop)
337
+ function generateLoopPoints(center, radius = 16, startAngle = Math.PI / 2, endAngle = startAngle + Math.PI * 2, pts = 96) {
338
+ const arr = [];
339
+ for (let i = 0; i <= pts; i++) {
340
+ const t = i / pts;
341
+ const ang = THREE.MathUtils.lerp(startAngle, endAngle, t);
342
+ const x = center.x + radius * Math.cos(ang);
343
+ const y = center.y + radius * Math.sin(ang);
344
+ const z = center.z;
345
+ arr.push(new THREE.Vector3(x, y, z));
346
+ }
347
+ return { points: arr, end: arr[arr.length - 1].clone() };
348
+ }
349
+
350
+ // 5) 多层交叉(在不同高度层之间左右穿梭)
351
+ function generateCrossoverPoints(start, spanX = 80, layers = 3, heightStep = 10, ptsPerLayer = 36) {
352
+ const arr = [];
353
+ const total = layers * ptsPerLayer;
354
+ for (let i = 0; i <= total; i++) {
355
+ const t = i / total;
356
+ const layerIndex = Math.floor(t * layers);
357
+ const y = start.y + layerIndex * heightStep;
358
+ const phase = (t * layers - layerIndex) * Math.PI * 2;
359
+ const x = start.x + (t - 0.5) * spanX + 10 * Math.sin(phase);
360
+ const z = start.z + 30 * Math.cos(phase);
361
+ arr.push(new THREE.Vector3(x, y, z));
362
+ }
363
+ return { points: arr, end: arr[arr.length - 1].clone() };
364
+ }
365
+
366
+ // 6) 弹珠台式分支(带震荡的通道段)
367
+ function generatePinballPoints(start, length = 100, swing = 14, freq = 2.0, ptsPerUnit = 1.2) {
368
+ const totalPts = Math.max(40, Math.floor(length * ptsPerUnit));
369
+ const arr = [];
370
+ for (let i = 0; i <= totalPts; i++) {
371
+ const t = i / totalPts;
372
+ const x = start.x + length * t;
373
+ const y = start.y + 3 * Math.sin(t * Math.PI * 2);
374
+ const z = start.z + swing * Math.sin(t * Math.PI * 2 * freq);
375
+ arr.push(new THREE.Vector3(x, y, z));
376
+ }
377
+ return { points: arr, end: arr[arr.length - 1].clone() };
378
+ }
379
+
380
+ // 7) 抛射与回归(抛物线段,落到更低轨道)
381
+ function generateLaunchReturnPoints(start, dx = 80, dy = 35, drop = 45, pts = 72) {
382
+ const arr = [];
383
+ for (let i = 0; i <= pts; i++) {
384
+ const t = i / pts;
385
+ const x = start.x + dx * t;
386
+ const y = start.y + dy * (4 * t * (1 - t)) - drop * t; // 抛物线加向下位移
387
+ const z = start.z;
388
+ arr.push(new THREE.Vector3(x, y, z));
389
+ }
390
+ return { points: arr, end: arr[arr.length - 1].clone() };
391
+ }
392
+
393
+ // 8) 过渡段(用于连接不同形状,保证切线连续)
394
+ function generateTransitionPoints(start, end, points = 20) {
395
+ const arr = [];
396
+ for (let i = 0; i <= points; i++) {
397
+ const t = i / points;
398
+ // 简单线性过渡,若需要更平滑可换用贝塞尔
399
+ arr.push(new THREE.Vector3(
400
+ THREE.MathUtils.lerp(start.x, end.x, t),
401
+ THREE.MathUtils.lerp(start.y, end.y, t),
402
+ THREE.MathUtils.lerp(start.z, end.z, t)
403
+ ));
404
+ }
405
+ return { points: arr, end: end.clone() };
406
+ }
407
+
408
+ // ======= 轨道与场景的构建 =======
409
+ function createRollerCoaster() {
410
+ // 组合所有段落的点
411
+ const allPoints = [];
412
+ let current = new THREE.Vector3(0, 0, 0);
413
+
414
+ // 1) 初��螺旋
415
+ let seg = generateHelixPoints(current, 20, 8, 24, 3.5, 60);
416
+ allPoints.push(...seg.points);
417
+ current.copy(seg.end);
418
+
419
+ // 2) 波浪段 + 过渡
420
+ seg = generateWavePoints(current, 120, 8, 1.6, 1.5);
421
+ allPoints.push(...seg.points);
422
+ current.copy(seg.end);
423
+
424
+ // 3) 过渡到漏斗
425
+ const beforeFunnel = new THREE.Vector3(current.x, current.y, current.z);
426
+ const funnelStart = new THREE.Vector3(beforeFunnel.x + 6, beforeFunnel.y, beforeFunnel.z + 6);
427
+ seg = generateTransitionPoints(beforeFunnel, funnelStart, 16);
428
+ allPoints.push(...seg.points);
429
+ current.copy(seg.end);
430
+
431
+ // 4) 漏斗下降
432
+ seg = generateFunnelPoints(current, 10, 3.5, 28, 2.5, 80);
433
+ allPoints.push(...seg.points);
434
+ current.copy(seg.end);
435
+
436
+ // 5) 过渡到环
437
+ const loopCenter = new THREE.Vector3(current.x + 6, current.y + 2, current.z + 6);
438
+ seg = generateTransitionPoints(current, loopCenter, 16);
439
+ allPoints.push(...seg.points);
440
+ current.copy(seg.end);
441
+
442
+ // 6) 垂直环
443
+ seg = generateLoopPoints(loopCenter, 16, Math.PI / 2, Math.PI / 2 + Math.PI * 2, 96);
444
+ allPoints.push(...seg.points);
445
+ current.copy(seg.end);
446
+
447
+ // 7) 过渡到多层交叉
448
+ const crossStart = new THREE.Vector3(current.x + 4, current.y + 2, current.z + 4);
449
+ seg = generateTransitionPoints(current, crossStart, 12);
450
+ allPoints.push(...seg.points);
451
+ current.copy(seg.end);
452
+
453
+ // 8) 多层交叉
454
+ seg = generateCrossoverPoints(current, 100, 3, 10, 42);
455
+ allPoints.push(...seg.points);
456
+ current.copy(seg.end);
457
+
458
+ // 9) 过渡到弹珠台段
459
+ const pinballStart = new THREE.Vector3(current.x + 3, current.y + 1, current.z + 3);
460
+ seg = generateTransitionPoints(current, pinballStart, 12);
461
+ allPoints.push(...seg.points);
462
+ current.copy(seg.end);
463
+
464
+ // 10) 弹珠台震荡段
465
+ seg = generatePinballPoints(current, 110, 14, 2.2, 1.3);
466
+ allPoints.push(...seg.points);
467
+ current.copy(seg.end);
468
+
469
+ // 11) 过渡到抛射段
470
+ const launchStart = new THREE.Vector3(current.x + 2, current.y, current.z);
471
+ seg = generateTransitionPoints(current, launchStart, 10);
472
+ allPoints.push(...seg.points);
473
+ current.copy(seg.end);
474
+
475
+ // 12) 抛射与回归
476
+ seg = generateLaunchReturnPoints(current, 90, 35, 55, 80);
477
+ allPoints.push(...seg.points);
478
+ current.copy(seg.end);
479
+
480
+ // 13) 最后回到起点附近(闭环)
481
+ const backToStart1 = new THREE.Vector3(current.x - 40, current.y - 8, current.z - 40);
482
+ const backToStart2 = new THREE.Vector3(0, 0, 0);
483
+ seg = generateTransitionPoints(current, backToStart1, 24);
484
+ allPoints.push(...seg.points);
485
+ current.copy(seg.end);
486
+ seg = generateTransitionPoints(current, backToStart2, 30);
487
+ allPoints.push(...seg.points);
488
+
489
+ // 使用 Chaikin 进行平滑,保证 G1 连续
490
+ const smoothed = chaikinSmoothPoints(allPoints, 2);
491
+
492
+ // 构建闭合曲线(chordal 类型,张力 0.15)
493
+ curve = new THREE.CatmullRomCurve3(smoothed, true, 'chordal', 0.15);
494
+
495
+ // 生成管道轨道
496
+ const tubularSegments = 1600; // 高分段,保证平滑
497
+ const radialSegments = 16; // 圆形截面
498
+ const pipeRadius = 0.7; // 管道半径(半径)
499
+ const trackGeo = new THREE.TubeGeometry(curve, tubularSegments, pipeRadius, radialSegments, true);
500
+
501
+ const trackMat = new THREE.MeshPhysicalMaterial({
502
+ color: 0xffffff,
503
+ roughness: 0.25,
504
+ metalness: 0.0,
505
+ transparent: true,
506
+ opacity: 0.7,
507
+ transmission: 0.2,
508
+ thickness: 0.6,
509
+ clearcoat: 0.9,
510
+ clearcoatRoughness: 0.2,
511
+ side: THREE.DoubleSide
512
+ });
513
+ trackMesh = new THREE.Mesh(trackGeo, trackMat);
514
+ trackMesh.castShadow = true;
515
+ trackMesh.receiveShadow = true;
516
+ scene.add(trackMesh);
517
+
518
+ // 动画小球
519
+ const ballGeo = new THREE.SphereGeometry(1.2, 32, 32);
520
+ const ballMat = new THREE.MeshStandardMaterial({
521
+ color: 0x2979ff,
522
+ roughness: 0.4,
523
+ metalness: 0.1
524
+ });
525
+ ball = new THREE.Mesh(ballGeo, ballMat);
526
+ ball.castShadow = true;
527
+ ball.receiveShadow = true;
528
+ scene.add(ball);
529
+
530
+ // 支架系统
531
+ supportsGroup = new THREE.Group();
532
+ scene.add(supportsGroup);
533
+ buildSupports(curve, pipeRadius);
534
+ HUD.ptCount.textContent = smoothed.length.toString();
535
+ HUD.supCount.textContent = supportsGroup.children.length.toString();
536
+ }
537
+
538
+ // ======= 支架系统 =======
539
+ function buildSupports(curve, trackRadius) {
540
+ const sampleEveryNPoints = 40; // 采样密度
541
+ const minHeightDiff = 10; // 轨道距离地面的最小高度
542
+ const pillarRadius = 0.18;
543
+ const footRadius = 0.65;
544
+
545
+ const pts = curve.getPoints(800); // 获取路径点
546
+ for (let i = 0; i < pts.length; i += sampleEveryNPoints) {
547
+ const p = pts[i];
548
+ const heightDiff = p.y - trackRadius; // 轨道外壁到地面的高度
549
+ if (heightDiff < minHeightDiff) continue;
550
+
551
+ // 圆柱支柱
552
+ const pillarGeo = new THREE.CylinderGeometry(pillarRadius, pillarRadius, heightDiff, 12);
553
+ const pillarMat = new THREE.MeshStandardMaterial({ color: 0x9e9e9e, roughness: 0.6, metalness: 0.7 });
554
+ const pillar = new THREE.Mesh(pillarGeo, pillarMat);
555
+ pillar.position.set(p.x, heightDiff / 2, p.z);
556
+ pillar.castShadow = true;
557
+ pillar.receiveShadow = true;
558
+ supportsGroup.add(pillar);
559
+
560
+ // 顶部圆盘
561
+ const topDiskGeo = new THREE.CylinderGeometry(trackRadius * 0.75, trackRadius * 0.75, 0.12, 16);
562
+ const topDisk = new THREE.Mesh(topDiskGeo, new THREE.MeshStandardMaterial({ color: 0xbdbdbd, roughness: 0.5, metalness: 0.6 }));
563
+ topDisk.position.set(p.x, p.y - trackRadius - 0.06, p.z);
564
+ topDisk.castShadow = true;
565
+ topDisk.receiveShadow = true;
566
+ supportsGroup.add(topDisk);
567
+
568
+ // 底部底座
569
+ const footGeo = new THREE.CylinderGeometry(footRadius, footRadius, 0.12, 16);
570
+ const foot = new THREE.Mesh(footGeo, new THREE.MeshStandardMaterial({ color: 0x9e9e9e, roughness: 0.65, metalness: 0.6 }));
571
+ foot.position.set(p.x, 0.06, p.z);
572
+ foot.castShadow = true;
573
+ foot.receiveShadow = true;
574
+ supportsGroup.add(foot);
575
+ }
576
+ }
577
+
578
+ // ======= 动画循环 =======
579
+ let fpsCounter = { last: performance.now(), frames: 0 };
580
+
581
+ function animate() {
582
+ requestAnimationFrame(animate);
583
+
584
+ const delta = clock.getDelta();
585
+ if (!isPaused) {
586
+ elapsed += delta;
587
+ }
588
+
589
+ // 小球沿轨道运动(恒速参数化:getPointAt)
590
+ if (curve && ball) {
591
+ const pathLength = 800; // 抽象长度,映射到 [0,1] 保持稳定
592
+ const s = (elapsed * speedMultiplier * 120) % pathLength; // 基础速度
593
+ const u = s / pathLength;
594
+
595
+ const pos = curve.getPointAt(u);
596
+ const tan = curve.getTangentAt(u);
597
+ ball.position.copy(pos);
598
+
599
+ // 球体朝向(可选)
600
+ const lookTarget = new THREE.Vector3().copy(pos).add(tan);
601
+ ball.lookAt(lookTarget);
602
+
603
+ // 第一人称:相机在小球后方/上方并看向前进方向
604
+ if (firstPerson) {
605
+ const backOffset = tan.clone().multiplyScalar(-3.2);
606
+ const upOffset = new THREE.Vector3(0, 1.3, 0);
607
+ const camPos = new THREE.Vector3().copy(pos).add(backOffset).add(upOffset);
608
+ camera.position.lerp(camPos, 0.25); // 平滑过渡
609
+ const ahead = new THREE.Vector3().copy(pos).add(tan.multiplyScalar(10)).add(new THREE.Vector3(0, 0.5, 0));
610
+ camera.lookAt(ahead);
611
+ } else {
612
+ controls.update();
613
+ }
614
+ }
615
+
616
+ renderer.render(scene, camera);
617
+
618
+ // 简易 FPS 统计
619
+ fpsCounter.frames++;
620
+ const now = performance.now();
621
+ if (now - fpsCounter.last >= 1000) {
622
+ const fps = Math.round((fpsCounter.frames * 1000) / (now - fpsCounter.last));
623
+ HUD.fps.textContent = fps.toString();
624
+ fpsCounter.frames = 0;
625
+ fpsCounter.last = now;
626
+ }
627
+ }
628
+ </script>
629
+ </body>
630
+ </html>