varunv2004 commited on
Commit
ae05fc2
·
verified ·
1 Parent(s): be4284e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1116 -18
index.html CHANGED
@@ -1,19 +1,1117 @@
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="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gemini AI Guide to 3D Game Creation</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script async src="https://ga.jspm.io/npm:es-module-shims@1.10.0/dist/es-module-shims.js"></script>
9
+ <script type="importmap">
10
+ {
11
+ "imports": {
12
+ "three": "https://cdn.jsdelivr.net/npm/three@0.164.1/build/three.module.js",
13
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.1/examples/jsm/",
14
+ "cannon-es": "https://unpkg.com/cannon-es@0.20.0/dist/cannon-es.js",
15
+ "cannon-es-debugger": "https://unpkg.com/cannon-es-debugger@1.0.0/dist/cannon-es-debugger.js"
16
+ }
17
+ }
18
+ </script>
19
+ <style>
20
+ body { font-family: 'Inter', sans-serif; background-color: #fdfbf8; color: #3f3c36; }
21
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&family=Roboto+Mono&display=swap');
22
+ .tab-active { border-color: #a58d6f; color: #a58d6f; background-color: #f5f2ed; }
23
+ .tab-inactive { border-color: transparent; color: #78716c; }
24
+ .content-section { display: none; }
25
+ .content-section.active { display: block; }
26
+ code { font-family: 'Roboto Mono', monospace; background-color: #f5f2ed; padding: 0.1rem 0.3rem; border-radius: 4px; color: #846c5b; }
27
+ pre { background-color: #f5f2ed; padding: 1rem; border-radius: 8px; overflow-x: auto; border: 1px solid #e7e5e4; }
28
+ .interactive-canvas-container { position: relative; width: 100%; max-width: 800px; margin-left: auto; margin-right: auto; height: 350px; max-height: 50vh; background-color: #e7e5e4; border-radius: 8px; }
29
+ .debug-console { background-color: #2d2a26; color: #e7e5e4; border-radius: 8px; height: 300px; padding: 1rem; overflow-y: auto; }
30
+ .debug-line { border-bottom: 1px solid #44403c; padding-bottom: 0.5rem; margin-bottom: 0.5rem; }
31
+ .debug-error { color: #f87171; }
32
+ .debug-info { color: #60a5fa; }
33
+ .debug-warn { color: #facc15; }
34
+ .accordion-button.open .accordion-arrow { transform: rotate(180deg); }
35
+ .accordion-arrow { transition: transform 0.2s ease-in-out; }
36
+ .accordion-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-in-out; }
37
+ </style>
38
+ </head>
39
+ <body class="antialiased">
40
+
41
+ <div class="container mx-auto p-4 sm:p-6 lg:p-8">
42
+
43
+ <header class="text-center mb-8">
44
+ <h1 class="text-4xl md:text-5xl font-bold text-gray-800" style="color: #654321;">Varun Mulay's Guide to 3D Web Development</h1>
45
+ <p class="mt-2 text-lg text-gray-600" style="color: #78716c;">An interactive guide to creating 3D games and simulations with Three.js.</p>
46
+ </header>
47
+
48
+ <!-- Tab Navigation -->
49
+ <nav class="mb-8 border-b border-gray-200">
50
+ <ul class="flex flex-wrap -mb-px text-sm font-medium text-center" id="tab-nav">
51
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="fundamentals">1. Fundamentals</button></li>
52
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="controls">2. Orbital Controls</button></li>
53
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="models">3. Importing Models</button></li>
54
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="physics">4. Physics with Cannon.js</button></li>
55
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="particles">5. Particle & Fluid Simulation</button></li>
56
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="debugging">6. Debugging</button></li>
57
+ <li><button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" data-tab="model-viewer">7. Model Viewer</button></li>
58
+ </ul>
59
+ </nav>
60
+
61
+ <!-- Content Sections -->
62
+ <main id="tab-content">
63
+ <!-- 1. Fundamentals -->
64
+ <section id="fundamentals" class="content-section space-y-6">
65
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">Three.js Fundamentals: The Core Components</h2>
66
+ <p>Every Three.js application, from a simple cube to a complex game, is built upon three core components: the <code>Scene</code>, the <code>Camera</code>, and the <code>Renderer</code>. Think of it like a movie set: the Scene is the stage where all your actors (3D objects) reside, the Camera is how the audience views the stage, and the Renderer is the crew that captures the camera's view and projects it onto the screen. This section provides an interactive demonstration of these foundational elements.</p>
67
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
68
+ <div>
69
+ <div id="fundamentals-canvas-container" class="interactive-canvas-container"></div>
70
+ <div class="mt-4 flex flex-wrap gap-2 justify-center">
71
+ <button id="move-x-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Move X</button>
72
+ <button id="move-y-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Move Y</button>
73
+ <button id="change-color-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Change Color</button>
74
+ </div>
75
+ </div>
76
+ <div>
77
+ <h3 class="text-xl font-semibold mb-2">Core Logic Explained</h3>
78
+ <p class="mb-4">Below is the essential JavaScript code. The interactive buttons on the left directly call functions that modify the cube's properties. See how simple commands can create dynamic changes in the 3D scene.</p>
79
+ <pre><code class="language-javascript">
80
+ // 1. Scene: The container for all objects
81
+ const scene = new THREE.Scene();
82
+ scene.background = new THREE.Color(0xf0f0f0);
83
+
84
+ // 2. Camera: Defines the viewpoint
85
+ const camera = new THREE.PerspectiveCamera(
86
+ 75, // Field of View
87
+ width / height, // Aspect Ratio
88
+ 0.1, // Near clip plane
89
+ 1000 // Far clip plane
90
+ );
91
+ camera.position.z = 5;
92
+
93
+ // 3. Renderer: Renders the scene
94
+ const renderer = new THREE.WebGLRenderer();
95
+ renderer.setSize(width, height);
96
+ document.body.appendChild(renderer.domElement);
97
+
98
+ // Add lighting for materials to be visible
99
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
100
+ scene.add(ambientLight);
101
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
102
+ directionalLight.position.set(5, 5, 5);
103
+ scene.add(directionalLight);
104
+
105
+ // Create an object (Mesh = Geometry + Material)
106
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
107
+ const material = new THREE.MeshStandardMaterial({
108
+ color: 0x846c5b
109
+ });
110
+ const cube = new THREE.Mesh(geometry, material);
111
+ scene.add(cube);
112
+
113
+ // Animation loop
114
+ function animate() {
115
+ requestAnimationFrame(animate);
116
+ cube.rotation.x += 0.01;
117
+ cube.rotation.y += 0.01;
118
+ renderer.render(scene, camera);
119
+ }
120
+ animate();
121
+
122
+ // --- Interactive Functions ---
123
+ function moveCubeX() {
124
+ cube.position.x += 0.5;
125
+ }
126
+ function changeCubeColor() {
127
+ cube.material.color.setHex(Math.random() * 0xffffff);
128
+ }
129
+ </code></pre>
130
+ </div>
131
+ </div>
132
+ </section>
133
+
134
+ <!-- 2. Orbital Controls -->
135
+ <section id="controls" class="content-section space-y-6">
136
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">Mastering Orbital Controls</h2>
137
+ <p>Static scenes are boring. <code>OrbitControls</code> is a powerful and essential tool that gives the user freedom to pan, zoom, and rotate the camera around a target. This makes your scene instantly interactive and explorable. However, its implementation has a few common pitfalls. This section explains the correct setup and provides a troubleshooting guide for the most frequent errors, ensuring a smooth user experience.</p>
138
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
139
+ <div>
140
+ <h3 class="text-xl font-semibold mb-2">Correct Implementation</h3>
141
+ <pre><code class="language-javascript">
142
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
143
+
144
+ // ... After setting up scene, camera, renderer
145
+
146
+ // Initialize controls
147
+ const controls = new OrbitControls(camera, renderer.domElement);
148
+
149
+ // Optional but recommended configurations
150
+ controls.enableDamping = true; // Adds inertia for a smoother feel
151
+ controls.dampingFactor = 0.05;
152
+ controls.screenSpacePanning = false; // Restricts panning to a plane
153
+ controls.minDistance = 2; // Min zoom
154
+ controls.maxDistance = 10; // Max zoom
155
+ controls.maxPolarAngle = Math.PI / 2; // Prevents looking from below ground
156
+
157
+ // CRITICAL: Update controls in the animation loop
158
+ function animate() {
159
+ requestAnimationFrame(animate);
160
+
161
+ // This is required if enableDamping is true
162
+ controls.update();
163
+
164
+ renderer.render(scene, camera);
165
+ }
166
+
167
+ animate();
168
+ </code></pre>
169
+ </div>
170
+ <div class="space-y-4">
171
+ <h3 class="text-xl font-semibold mb-2">Troubleshooting Common Errors</h3>
172
+ <div class="accordion-item bg-white border border-gray-200 rounded-md">
173
+ <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
174
+ <span>My controls feel laggy or don't stop immediately.</span>
175
+ <span class="accordion-arrow transform transition-transform">▼</span>
176
+ </button>
177
+ <div class="accordion-content px-4 pb-4">
178
+ <p><strong>Cause:</strong> You have set <code>controls.enableDamping = true</code> but forgotten to call <code>controls.update()</code> inside your animation loop.</p>
179
+ <p><strong>Fix:</strong> Add <code>controls.update()</code> to your <code>animate</code> function. This ensures the controls smoothly decelerate each frame.</p>
180
+ </div>
181
+ </div>
182
+ <div class="accordion-item bg-white border border-gray-200 rounded-md">
183
+ <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
184
+ <span>The camera zooms in/out but doesn't rotate.</span>
185
+ <span class="accordion-arrow transform transition-transform">▼</span>
186
+ </button>
187
+ <div class="accordion-content px-4 pb-4">
188
+ <p><strong>Cause:</strong> The OrbitControls script has not loaded correctly, or you have a JavaScript error elsewhere that is stopping the script execution.</p>
189
+ <p><strong>Fix:</strong> Check your browser's developer console (F12) for errors. Ensure the path to <code>OrbitControls.js</code> in your import is correct and that the Three.js library itself has loaded first.</p>
190
+ </div>
191
+ </div>
192
+ <div class="accordion-item bg-white border border-gray-200 rounded-md">
193
+ <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
194
+ <span>My camera can go below the "ground" plane.</span>
195
+ <span class="accordion-arrow transform transition-transform">▼</span>
196
+ </button>
197
+ <div class="accordion-content px-4 pb-4">
198
+ <p><strong>Cause:</strong> The default polar angle allows for 360-degree vertical rotation.</p>
199
+ <p><strong>Fix:</strong> Limit the vertical rotation by setting <code>controls.maxPolarAngle</code>. For example, <code>controls.maxPolarAngle = Math.PI / 2;</code> prevents the camera from going below the horizontal equator.</p>
200
+ </div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </section>
205
+
206
+ <!-- 3. Importing Models -->
207
+ <section id="models" class="content-section space-y-6">
208
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">Importing 3D Models (.glb/.gltf)</h2>
209
+ <p>Primitives are great for learning, but real projects use complex 3D models created in software like Blender or Maya. The most common and recommended format for the web is <code>gLTF</code> (or its binary version, <code>.glb</code>) because it's optimized for fast loading and rendering. Use the button below to upload a model from your computer. You can use the sliders to interactively adjust its position, rotation, and scale, which are the fundamental transformations you'll apply to any object in your scene.</p>
210
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
211
+ <div>
212
+ <div id="models-canvas-container" class="interactive-canvas-container"></div>
213
+ <div class="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4 p-4 bg-white border rounded-md">
214
+ <div>
215
+ <label for="model-pos-x" class="block text-sm font-medium">Position X</label>
216
+ <input type="range" id="model-pos-x" min="-5" max="5" step="0.1" value="0" class="w-full">
217
+ </div>
218
+ <div>
219
+ <label for="model-rot-y" class="block text-sm font-medium">Rotation Y</label>
220
+ <input type="range" id="model-rot-y" min="-3.14" max="3.14" step="0.1" value="0" class="w-full">
221
+ </div>
222
+ <div>
223
+ <label for="model-scale" class="block text-sm font-medium">Scale</label>
224
+ <input type="range" id="model-scale" min="0.1" max="3" step="0.1" value="1.5" class="w-full">
225
+ </div>
226
+ </div>
227
+ <div class="mt-4 flex flex-wrap gap-2 justify-center">
228
+ <label class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition cursor-pointer">
229
+ Upload Model (.glb/.gltf)
230
+ <input type="file" id="model-upload" class="hidden" accept=".glb, .gltf">
231
+ </label>
232
+ <span id="loading-indicator" class="px-4 py-2 text-gray-500 hidden">Loading...</span>
233
+ </div>
234
+ </div>
235
+ <div>
236
+ <h3 class="text-xl font-semibold mb-2">Model Loader & Transformation Code</h3>
237
+ <p class="mb-4">Loading models is an asynchronous operation. You use a specific loader (<code>GLTFLoader</code> here), provide a path to your model, and define a callback function that executes once the model has loaded successfully. Inside this callback, you can add the model to the scene and manipulate it.</p>
238
+ <pre><code id="model-code-output" class="language-javascript">
239
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
240
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
241
+
242
+ const loader = new GLTFLoader();
243
+ let model;
244
+
245
+ // Event listener for file input
246
+ document.getElementById('model-upload').addEventListener('change', (event) => {
247
+ const file = event.target.files[0];
248
+ if (!file) return;
249
+
250
+ // Create a local URL for the uploaded file
251
+ const url = URL.createObjectURL(file);
252
+
253
+ // Display loading indicator
254
+ document.getElementById('loading-indicator').classList.remove('hidden');
255
+
256
+ // Remove any previously loaded model
257
+ if (model) {
258
+ scene.remove(model);
259
+ model = null;
260
+ }
261
+
262
+ loader.load(
263
+ url,
264
+ function (gltf) {
265
+ model = gltf.scene;
266
+
267
+ // --- Transformations applied here ---
268
+ model.position.set(0, -1, 0);
269
+ model.rotation.y = 0;
270
+ model.scale.set(1.5, 1.5, 1.5);
271
+
272
+ scene.add(model);
273
+
274
+ // Clean up the temporary URL
275
+ URL.revokeObjectURL(url);
276
+
277
+ // Hide loading indicator
278
+ document.getElementById('loading-indicator').classList.add('hidden');
279
+ },
280
+ undefined, // onProgress callback (optional)
281
+ function (error) {
282
+ console.error('An error occurred while loading the model:', error);
283
+ document.getElementById('loading-indicator').classList.add('hidden');
284
+ }
285
+ );
286
+ });
287
+
288
+ // In your update logic (called by sliders):
289
+ function updateModel() {
290
+ if (model) {
291
+ model.position.x = /* slider value */;
292
+ model.rotation.y = /* slider value */;
293
+ const scale = /* slider value */;
294
+ model.scale.set(scale, scale, scale);
295
+ }
296
+ }
297
+ </code></pre>
298
+ </div>
299
+ </div>
300
+ </section>
301
+
302
+ <!-- 4. Physics with Cannon.js -->
303
+ <section id="physics" class="content-section space-y-6">
304
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">The Physics Engine: Cannon.js</h2>
305
+ <p>A static scene is a portfolio piece, but a game needs dynamic interaction and realistic movement. Three.js is a rendering engine, so it doesn't handle physics. That's where a separate library like **Cannon.js** comes in. Cannon.js handles the backend calculations for gravity, collisions, and forces, while you use its results to update the positions and rotations of your visible Three.js objects. This section demonstrates how to use the **cannon-es-debugger** to visualize physics shapes and uses a collision event listener to provide immediate feedback on interactions. This is critical for debugging why objects might "fall through" floors or collide unexpectedly.</p>
306
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
307
+ <div>
308
+ <div id="physics-canvas-container" class="interactive-canvas-container"></div>
309
+ <div class="mt-4 flex flex-wrap gap-2 justify-center">
310
+ <button id="drop-ball-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Ball</button>
311
+ <label class="flex items-center space-x-2 px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm">
312
+ <input type="checkbox" id="physics-debug-toggle" class="form-checkbox h-4 w-4 text-gray-600 transition-colors">
313
+ <span class="text-sm font-medium text-gray-700">Debug Physics</span>
314
+ </label>
315
+ </div>
316
+ </div>
317
+ <div>
318
+ <h3 class="text-xl font-semibold mb-2">Connecting Physics to Rendering</h3>
319
+ <p class="mb-4">The core loop of a physics-based game involves two key steps: first, advance the physics world, then update the visual objects based on the new physics positions. The code below shows how to set up the debug visualization and react to collisions.</p>
320
+ <pre><code class="language-javascript">
321
+ import * as CANNON from 'cannon-es';
322
+ import * as CannonDebugRenderer from 'cannon-es-debugger';
323
+
324
+ // ... Three.js setup ...
325
+
326
+ // 1. Create the Physics World
327
+ const world = new CANNON.World({
328
+ gravity: new CANNON.Vec3(0, -9.82, 0) // Simulates gravity
329
+ });
330
+
331
+ // 2. Initialize the debug renderer
332
+ const cannonDebugRenderer = new CannonDebugRenderer.default(scene, world);
333
+
334
+ // 3. Create the physical ground plane
335
+ const groundShape = new CANNON.Plane();
336
+ const groundBody = new CANNON.Body({ mass: 0 }); // mass 0 makes it static
337
+ groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
338
+ world.addBody(groundBody);
339
+
340
+ // Function to create a new sphere
341
+ function createSphere() {
342
+ const radius = 0.5;
343
+ const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0x846c5b });
344
+ const sphereMesh = new THREE.Mesh(new THREE.SphereGeometry(radius), sphereMaterial);
345
+ sphereMesh.position.y = 10;
346
+ scene.add(sphereMesh);
347
+
348
+ const sphereBody = new CANNON.Body({
349
+ mass: 1,
350
+ shape: new CANNON.Sphere(radius)
351
+ });
352
+ sphereBody.position.y = 10;
353
+ world.addBody(sphereBody);
354
+
355
+ // Add a collision listener
356
+ sphereBody.addEventListener('collide', (event) => {
357
+ // Flash the sphere red on collision
358
+ sphereMesh.material.color.setHex(0xff0000);
359
+ setTimeout(() => {
360
+ sphereMesh.material.color.setHex(0x846c5b);
361
+ }, 200);
362
+ });
363
+ }
364
+
365
+ // 4. Update both in the animation loop
366
+ function animate() {
367
+ requestAnimationFrame(animate);
368
+
369
+ // Step the physics world forward in time
370
+ world.fixedStep();
371
+
372
+ // Update the visual representation
373
+ cannonDebugRenderer.update();
374
+
375
+ // Sync each Three.js mesh with its Cannon.js body
376
+ // ... loop through meshes and bodies ...
377
+
378
+ renderer.render(scene, camera);
379
+ }
380
+ animate();
381
+ </code></pre>
382
+ <h3 class="text-xl font-semibold mt-4 mb-2">Common Cannon.js Issues</h3>
383
+ <div class="accordion-item bg-white border border-gray-200 rounded-md">
384
+ <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
385
+ <span>My objects fall forever through the floor.</span>
386
+ <span class="accordion-arrow transform transition-transform">▼</span>
387
+ </button>
388
+ <div class="accordion-content px-4 pb-4">
389
+ <p><strong>Cause:</strong> You need a static, non-moving ground plane for the objects to collide with. The objects are falling because there is no collision body for them to interact with.</p>
390
+ <p><strong>Fix:</strong> Create a ground body in Cannon.js with a mass of 0. This makes it static and unmovable, effectively creating an infinite floor. For example: <code>const groundBody = new CANNON.Body({ mass: 0 }); groundBody.addShape(new CANNON.Plane()); groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0); world.addBody(groundBody);</code></p>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </section>
396
+
397
+ <!-- 5. Particle & Fluid Simulation -->
398
+ <section id="particles" class="content-section space-y-6">
399
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">Buoyancy and Density Simulation</h2>
400
+ <p>Simulating how different materials react to water can be a challenge. In this simple demonstration, we've created a custom particle system to show the difference between wood and metal. Wood, being less dense, floats on the surface, while metal, being denser, continues to sink. Click the buttons below to see the difference.</p>
401
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
402
+ <div>
403
+ <div id="particles-canvas-container" class="interactive-canvas-container"></div>
404
+ <div class="mt-4 flex flex-wrap gap-2 justify-center">
405
+ <button id="drop-wood-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Wood</button>
406
+ <button id="drop-metal-btn" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Metal</button>
407
+ </div>
408
+ </div>
409
+ <div>
410
+ <h3 class="text-xl font-semibold mb-2">Simulation Logic</h3>
411
+ <p class="mb-4">Here's a breakdown of the code that creates the simulation. The particle system is handled by a custom array that stores each particle's type. The animation loop then applies a different "physics" rule to each particle once it hits the water plane.</p>
412
+ <pre><code class="language-javascript">
413
+ import { Water } from 'three/addons/objects/Water.js';
414
+
415
+ // ... Three.js setup ...
416
+
417
+ // Create the water plane
418
+ const waterGeometry = new THREE.PlaneGeometry(2000, 2000);
419
+ const water = new Water(
420
+ waterGeometry,
421
+ {
422
+ textureWidth: 512,
423
+ textureHeight: 512,
424
+ waterNormals: new THREE.TextureLoader().load('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/waternormals.jpg', function (texture) {
425
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
426
+ }),
427
+ sunDirection: new THREE.Vector3(0, 1, 0),
428
+ sunColor: 0x48494b,
429
+ waterColor: 0x005577,
430
+ distortionScale: 3.7,
431
+ fog: scene.fog !== undefined
432
+ }
433
+ );
434
+ water.rotation.x = -Math.PI / 2;
435
+ scene.add(water);
436
+
437
+ // Setup the particle system
438
+ const particleCount = 500;
439
+ const positions = new Float32Array(particleCount * 3);
440
+ const velocities = new Float32Array(particleCount * 3);
441
+ const particleTypes = new Array(particleCount).fill(null);
442
+ const colors = new Float32Array(particleCount * 3);
443
+
444
+ const particleGeometry = new THREE.BufferGeometry();
445
+ particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
446
+ particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
447
+
448
+ const particleMaterial = new THREE.PointsMaterial({
449
+ size: 0.5,
450
+ vertexColors: true,
451
+ sizeAttenuation: true,
452
+ transparent: true,
453
+ opacity: 0.9,
454
+ blending: THREE.AdditiveBlending
455
+ });
456
+ const particleSystem = new THREE.Points(particleGeometry, particleMaterial);
457
+ scene.add(particleSystem);
458
+
459
+ const woodColor = new THREE.Color(0x8B4513); // Brown color for wood
460
+ const metalColor = new THREE.Color(0x708090); // Gray color for metal
461
+
462
+ let particleCursor = 0;
463
+
464
+ // Function to spawn particles based on type
465
+ const spawnParticles = (type) => {
466
+ const count = 50;
467
+ const spread = 20;
468
+ const startY = 15;
469
+
470
+ for (let i = 0; i < count; i++) {
471
+ const idx = particleCursor * 3;
472
+ positions[idx] = (Math.random() - 0.5) * spread;
473
+ positions[idx + 1] = startY + Math.random() * 5;
474
+ positions[idx + 2] = (Math.random() - 0.5) * spread;
475
+
476
+ velocities[idx] = 0;
477
+ velocities[idx + 1] = -0.3 - Math.random() * 0.3;
478
+ velocities[idx + 2] = 0;
479
+
480
+ particleTypes[particleCursor] = type;
481
+
482
+ if (type === 'wood') {
483
+ woodColor.toArray(colors, idx);
484
+ } else {
485
+ metalColor.toArray(colors, idx);
486
+ }
487
+
488
+ particleCursor = (particleCursor + 1) % particleCount;
489
+ }
490
+ particleGeometry.attributes.position.needsUpdate = true;
491
+ particleGeometry.attributes.color.needsUpdate = true;
492
+ };
493
+
494
+ // Event listeners for the buttons
495
+ document.getElementById('drop-wood-btn').onclick = () => spawnParticles('wood');
496
+ document.getElementById('drop-metal-btn').onclick = () => spawnParticles('metal');
497
+
498
+ // The main animation loop
499
+ function animate() {
500
+ requestAnimationFrame(animate);
501
+
502
+ controls.update();
503
+ water.material.uniforms['time'].value += 1.0 / 60.0;
504
+
505
+ // Update particle positions and apply physics
506
+ for (let i = 0; i < particleCount * 3; i += 3) {
507
+ const particleIndex = i / 3;
508
+ const currentY = positions[i + 1];
509
+
510
+ // If the particle is below the water surface
511
+ if (currentY < water.position.y) {
512
+ if (particleTypes[particleIndex] === 'wood') {
513
+ // Wood floats: Stop it from sinking further
514
+ positions[i + 1] = water.position.y;
515
+ velocities[i + 1] = 0;
516
+ } else if (particleTypes[particleIndex] === 'metal') {
517
+ // Metal sinks: Continue downward motion with some resistance
518
+ velocities[i + 1] *= 0.98;
519
+ }
520
+ } else {
521
+ // In freefall, apply simple gravity
522
+ velocities[i + 1] -= 0.005;
523
+ }
524
+
525
+ positions[i] += velocities[i];
526
+ positions[i + 1] += velocities[i + 1];
527
+ positions[i + 2] += velocities[i + 2];
528
+ }
529
+
530
+ particleGeometry.attributes.position.needsUpdate = true;
531
+ renderer.render(scene, camera);
532
+ }
533
+ animate();
534
+ </code></pre>
535
+ </div>
536
+ </div>
537
+ </section>
538
+
539
+ <!-- 6. Debugging -->
540
+ <section id="debugging" class="content-section space-y-6">
541
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">A Game Developer's Debugging Toolkit</h2>
542
+ <p>When things go wrong in 3D, the errors can be cryptic. A blank screen is a common symptom for many different problems. Effective debugging requires understanding what the errors mean in a 3D context. This section simulates a debugging console with common errors you might encounter. Click on each error line to understand its likely cause and how to approach fixing it, turning you into a more effective problem solver.</p>
543
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
544
+ <div class="space-y-4">
545
+ <h3 class="text-xl font-semibold mb-2">Interactive Debug Console</h3>
546
+ <div class="debug-console" id="debug-console">
547
+ <div class="debug-line debug-info cursor-pointer" data-fix="fix1">▷ INFO: THREE.WebGLRenderer 164</div>
548
+ <div class="debug-line debug-warn cursor-pointer" data-fix="fix2">▷ WARN: Scene has no lights. Objects may appear black.</div>
549
+ <div class="debug-line debug-error cursor-pointer" data-fix="fix3">▷ ERROR: TypeError: Cannot read properties of undefined (reading 'scene')</div>
550
+ <div class="debug-line debug-error cursor-pointer" data-fix="fix4">▷ ERROR: Failed to load resource: net::ERR_FILE_NOT_FOUND Horse.glb</div>
551
+ <div class="debug-line debug-info cursor-pointer" data-fix="fix5">▷ INFO: Animation loop started.</div>
552
+ <div class="debug-line debug-error cursor-pointer" data-fix="fix6">▷ ERROR: Uncaught TypeError: Cannot set properties of null (setting 'position')</div>
553
+ </div>
554
+ </div>
555
+ <div class="p-4 bg-white border rounded-md" id="debug-fix-display">
556
+ <h3 class="text-xl font-semibold mb-2">Analysis & Fix</h3>
557
+ <p class="text-gray-500">Click on an error in the console to see a detailed explanation and solution here.</p>
558
+ </div>
559
+ </div>
560
+ <div id="fixes" class="hidden">
561
+ <div id="fix1">
562
+ <h4 class="font-bold">Renderer Information</h4>
563
+ <p>This is a standard startup message confirming that the Three.js renderer has been successfully initialized. It's not an error. If you don't see this, it's likely Three.js itself failed to load.</p>
564
+ </div>
565
+ <div id="fix2">
566
+ <h4 class="font-bold">Missing Lights</h4>
567
+ <p><strong>Cause:</strong> Most materials, like <code>MeshStandardMaterial</code> or <code>MeshPhongMaterial</code>, require light to be visible. Without any lights in the scene, objects using these materials will render as black.</p>
568
+ <p><strong>Fix:</strong> Add at least one light to your scene. An <code>AmbientLight</code> provides basic, flat illumination, while a <code>DirectionalLight</code> simulates a distant light source like the sun.</p>
569
+ <pre><code>const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
570
+ scene.add(ambientLight);
571
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
572
+ directionalLight.position.set(5, 10, 7.5);
573
+ scene.add(directionalLight);</code></pre>
574
+ </div>
575
+ <div id="fix3">
576
+ <h4 class="font-bold">Undefined Property Error</h4>
577
+ <p><strong>Cause:</strong> This typically happens when your model loader's callback function returns, but the model data (<code>gltf</code> in this case) is not what you expect. Your code tries to access <code>gltf.scene</code>, but <code>gltf</code> is undefined.</p>
578
+ <p><strong>Fix:</strong> This is often a symptom of a failed model load. Check the console for an earlier error (like a 404 Not Found) that indicates the model file itself couldn't be fetched. The loader failed, so the callback received nothing.</p>
579
+ </div>
580
+ <div id="fix4">
581
+ <h4 class="font-bold">Resource Not Found</h4>
582
+ <p><strong>Cause:</strong> The path provided to the model loader (e.g., <code>GLTFLoader.load('path/to/model.glb')</code>) is incorrect. The browser cannot find the file at that location.</p>
583
+ <p><strong>Fix:</strong> Double-check the file path. Is it relative or absolute? Is there a typo? Open your browser's "Network" tab in the developer tools to see the exact URL it tried to fetch and the 404 error response.</p>
584
+ </div>
585
+ <div id="fix5">
586
+ <h4 class="font-bold">Animation Loop Started</h4>
587
+ <p>This is a useful debugging message you can add yourself with <code>console.log()</code> to confirm that your <code>animate</code> function is being called. If your scene is static when it should be moving, and you don't see this message, you've likely forgotten to call <code>animate()</code> to start the loop.</p>
588
+ </div>
589
+ <div id="fix6">
590
+ <h4 class="font-bold">`null` Property Error</h4>
591
+ <p><strong>Cause:</strong> You are trying to use a variable before the object it represents has been fully created and added to the scene. For example, if you declare a variable <code>let cube;</code> at the top, then try to modify <code>cube.position</code> outside of the function that creates the cube, it will be <code>null</code> or <code>undefined</code>.</p>
592
+ <p><strong>Fix:</strong> Make sure your object is initialized before you try to access its properties. A common solution is to wrap your code that interacts with the object in a conditional statement like <code>if (cube) { ... }</code>, or to ensure that your functions are called only after the object has been fully initialized.</p>
593
+ </div>
594
+ </div>
595
+ </section>
596
+
597
+ <!-- 7. Model Viewer -->
598
+ <section id="model-viewer" class="content-section space-y-6">
599
+ <h2 class="text-3xl font-bold" style="color: #a58d6f;">The Easy Way: Google's &lt;model-viewer&gt;</h2>
600
+ <p>For some use cases, like displaying a single product or a piece of art, a full Three.js scene is overkill. Google's <code>&lt;model-viewer&gt;</code> is a web component that lets you declaratively add a 3D model to a webpage with minimal code. It's incredibly powerful for simple showcases, offering features like AR placement, animations, and camera controls right out of the box. Understanding when to use this tool can save you significant development time.</p>
601
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
602
+ <div>
603
+ <h3 class="text-xl font-semibold mb-2">Live Example</h3>
604
+ <!-- Note: The script for model-viewer is added at the end of the body -->
605
+ <model-viewer src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
606
+ alt="A 3D model of an astronaut"
607
+ ar
608
+ auto-rotate
609
+ camera-controls
610
+ style="width: 100%; height: 400px; background-color: #f0f0f0; border-radius: 8px;">
611
+ </model-viewer>
612
+ </div>
613
+ <div>
614
+ <h3 class="text-xl font-semibold mb-2">When to Use Which?</h3>
615
+ <div class="space-y-4">
616
+ <div>
617
+ <h4 class="font-semibold text-lg" style="color: #846c5b;">Use <code>&lt;model-viewer&gt;</code> when:</h4>
618
+ <ul class="list-disc list-inside text-gray-700">
619
+ <li>You need to display a single, pre-made 3D model.</li>
620
+ <li>The main goal is showcasing the model (e.g., e-commerce, portfolio).</li>
621
+ <li>You want easy integration with Augmented Reality (AR).</li>
622
+ <li>You prefer a declarative HTML approach over writing JavaScript.</li>
623
+ </ul>
624
+ </div>
625
+ <div>
626
+ <h4 class="font-semibold text-lg" style="color: #846c5b;">Use <code>Three.js</code> when:</h4>
627
+ <ul class="list-disc list-inside text-gray-700">
628
+ <li>You are building a game or a highly interactive simulation.</li>
629
+ <li>You need to manage multiple objects, physics, and complex logic.</li>
630
+ <li>You require custom shaders, post-processing effects, or particle systems.</li>
631
+ <li>You need full programmatic control over every aspect of the 3D scene.</li>
632
+ </ul>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ </div>
637
+ </section>
638
+ </main>
639
+ </div>
640
+
641
+ <!-- Model Viewer Script -->
642
+ <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
643
+
644
+ <script type="module">
645
+ import * as THREE from 'three';
646
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
647
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
648
+ import * as CANNON from 'cannon-es';
649
+ import * as CannonDebugRenderer from 'cannon-es-debugger';
650
+ import { Water } from 'three/addons/objects/Water.js';
651
+ import { Reflector } from 'three/addons/objects/Reflector.js';
652
+
653
+ // --- Tab Navigation Logic ---
654
+ const tabNav = document.getElementById('tab-nav');
655
+ const tabContent = document.getElementById('tab-content');
656
+ const sections = tabContent.querySelectorAll('.content-section');
657
+ const tabs = tabNav.querySelectorAll('button');
658
+
659
+ function switchTab(targetTabId) {
660
+ sections.forEach(section => {
661
+ section.classList.toggle('active', section.id === targetTabId);
662
+ });
663
+ tabs.forEach(tab => {
664
+ tab.classList.toggle('tab-active', tab.dataset.tab === targetTabId);
665
+ tab.classList.toggle('tab-inactive', tab.dataset.tab !== targetTabId);
666
+ });
667
+ }
668
+
669
+ tabNav.addEventListener('click', (e) => {
670
+ if (e.target.tagName === 'BUTTON') {
671
+ switchTab(e.target.dataset.tab);
672
+ }
673
+ });
674
+
675
+ switchTab('fundamentals');
676
+
677
+ // --- Accordion Logic ---
678
+ const accordions = document.querySelectorAll('.accordion-button');
679
+ accordions.forEach(button => {
680
+ button.addEventListener('click', () => {
681
+ const content = button.nextElementSibling;
682
+ button.classList.toggle('open');
683
+ if (content.style.maxHeight) {
684
+ content.style.maxHeight = null;
685
+ } else {
686
+ content.style.maxHeight = content.scrollHeight + "px";
687
+ }
688
+ });
689
+ });
690
+
691
+ // --- Scene 1: Fundamentals ---
692
+ function initFundamentals() {
693
+ const container = document.getElementById('fundamentals-canvas-container');
694
+ if (!container || container.querySelector('canvas')) return;
695
+
696
+ const scene = new THREE.Scene();
697
+ scene.background = new THREE.Color(0xe7e5e4);
698
+
699
+ const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
700
+ camera.position.z = 3;
701
+
702
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
703
+ renderer.setSize(container.clientWidth, container.clientHeight);
704
+ container.appendChild(renderer.domElement);
705
+
706
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
707
+ scene.add(ambientLight);
708
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
709
+ directionalLight.position.set(5, 5, 5);
710
+ scene.add(directionalLight);
711
+
712
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
713
+ const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
714
+ const cube = new THREE.Mesh(geometry, material);
715
+ scene.add(cube);
716
+
717
+ document.getElementById('move-x-btn').onclick = () => { cube.position.x = (cube.position.x + 0.5) % 3; };
718
+ document.getElementById('move-y-btn').onclick = () => { cube.position.y = (cube.position.y + 0.5) % 2; };
719
+ document.getElementById('change-color-btn').onclick = () => { cube.material.color.setHex(Math.random() * 0xffffff); };
720
+
721
+ function animate() {
722
+ requestAnimationFrame(animate);
723
+ cube.rotation.x += 0.005;
724
+ cube.rotation.y += 0.005;
725
+ renderer.render(scene, camera);
726
+ }
727
+ animate();
728
+
729
+ window.addEventListener('resize', () => {
730
+ camera.aspect = container.clientWidth / container.clientHeight;
731
+ camera.updateProjectionMatrix();
732
+ renderer.setSize(container.clientWidth, container.clientHeight);
733
+ });
734
+ }
735
+
736
+ // --- Scene 2: Model Importing ---
737
+ function initModels() {
738
+ const container = document.getElementById('models-canvas-container');
739
+ if (!container || container.querySelector('canvas')) return;
740
+
741
+ const scene = new THREE.Scene();
742
+ scene.background = new THREE.Color(0xe7e5e4);
743
+
744
+ const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
745
+ camera.position.set(0, 1.5, 4);
746
+
747
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
748
+ renderer.setSize(container.clientWidth, container.clientHeight);
749
+ container.appendChild(renderer.domElement);
750
+
751
+ const controls = new OrbitControls(camera, renderer.domElement);
752
+ controls.enableDamping = true;
753
+ controls.target.set(0, 0.5, 0);
754
+
755
+ const ambientLight = new THREE.AmbientLight(0xffffff, 2);
756
+ scene.add(ambientLight);
757
+ const dirLight = new THREE.DirectionalLight(0xffffff, 3);
758
+ dirLight.position.set(5, 10, 7.5);
759
+ scene.add(dirLight);
760
+
761
+ const loader = new GLTFLoader();
762
+ let model = null;
763
+ let currentUrl = null;
764
+
765
+ // Function to load and display the model
766
+ function loadAndDisplayModel(url) {
767
+ // Remove any previously loaded model
768
+ if (model) {
769
+ scene.remove(model);
770
+ model = null;
771
+ }
772
+
773
+ document.getElementById('loading-indicator').classList.remove('hidden');
774
+
775
+ loader.load(
776
+ url,
777
+ function (gltf) {
778
+ model = gltf.scene;
779
+
780
+ model.position.y = -1;
781
+ updateModelTransform();
782
+ scene.add(model);
783
+
784
+ document.getElementById('loading-indicator').classList.add('hidden');
785
+ },
786
+ undefined,
787
+ function (error) {
788
+ console.error('An error occurred while loading the model:', error);
789
+ document.getElementById('loading-indicator').classList.add('hidden');
790
+ }
791
+ );
792
+ }
793
+
794
+ const uploadInput = document.getElementById('model-upload');
795
+ uploadInput.addEventListener('change', (event) => {
796
+ const file = event.target.files[0];
797
+ if (!file) return;
798
+
799
+ if (currentUrl) {
800
+ URL.revokeObjectURL(currentUrl);
801
+ }
802
+ currentUrl = URL.createObjectURL(file);
803
+ loadAndDisplayModel(currentUrl);
804
+ });
805
+
806
+ // Load a default model on startup
807
+ loadAndDisplayModel('https://cdn.jsdelivr.net/npm/three@0.164.1/examples/models/gltf/Horse.glb');
808
+
809
+ const posXSlider = document.getElementById('model-pos-x');
810
+ const rotYSlider = document.getElementById('model-rot-y');
811
+ const scaleSlider = document.getElementById('model-scale');
812
+
813
+ function updateModelTransform() {
814
+ if(model) {
815
+ const scale = parseFloat(scaleSlider.value);
816
+ model.scale.set(scale, scale, scale);
817
+ model.position.x = parseFloat(posXSlider.value);
818
+ model.rotation.y = parseFloat(rotYSlider.value);
819
+ }
820
+ }
821
+
822
+ posXSlider.addEventListener('input', updateModelTransform);
823
+ rotYSlider.addEventListener('input', updateModelTransform);
824
+ scaleSlider.addEventListener('input', updateModelTransform);
825
+
826
+ function animate() {
827
+ requestAnimationFrame(animate);
828
+ controls.update();
829
+ renderer.render(scene, camera);
830
+ }
831
+ animate();
832
+
833
+ window.addEventListener('resize', () => {
834
+ camera.aspect = container.clientWidth / container.clientHeight;
835
+ camera.updateProjectionMatrix();
836
+ renderer.setSize(container.clientWidth, container.clientHeight);
837
+ });
838
+ }
839
+
840
+ // --- Scene 3: Physics with Cannon.js ---
841
+ function initPhysics() {
842
+ const container = document.getElementById('physics-canvas-container');
843
+ if (!container || container.querySelector('canvas')) return;
844
+
845
+ const scene = new THREE.Scene();
846
+ scene.background = new THREE.Color(0xe7e5e4);
847
+
848
+ const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
849
+ camera.position.set(0, 5, 10);
850
+
851
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
852
+ renderer.setSize(container.clientWidth, container.clientHeight);
853
+ container.appendChild(renderer.domElement);
854
+
855
+ const controls = new OrbitControls(camera, renderer.domElement);
856
+ controls.enableDamping = true;
857
+
858
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
859
+ scene.add(ambientLight);
860
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
861
+ directionalLight.position.set(5, 10, 7.5);
862
+ scene.add(directionalLight);
863
+
864
+ // Cannon.js world setup
865
+ const world = new CANNON.World({
866
+ gravity: new CANNON.Vec3(0, -9.82, 0)
867
+ });
868
+ const cannonDebugRenderer = new CannonDebugRenderer.default(scene, world);
869
+
870
+ // Cannon.js ground plane
871
+ const groundShape = new CANNON.Plane();
872
+ const groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
873
+ groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
874
+ world.addBody(groundBody);
875
+
876
+ const meshes = [];
877
+ const bodies = [];
878
+
879
+ const createSphere = () => {
880
+ const radius = 0.5;
881
+ const sphereGeometry = new THREE.SphereGeometry(radius, 32, 32);
882
+ const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0x846c5b });
883
+ const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
884
+ sphereMesh.position.y = 10;
885
+ scene.add(sphereMesh);
886
+ meshes.push(sphereMesh);
887
+
888
+ const sphereBody = new CANNON.Body({
889
+ mass: 1,
890
+ shape: new CANNON.Sphere(radius)
891
+ });
892
+ sphereBody.position.y = 10;
893
+ world.addBody(sphereBody);
894
+ bodies.push(sphereBody);
895
+
896
+ // Add collision listener
897
+ sphereBody.addEventListener('collide', (event) => {
898
+ sphereMesh.material.color.setHex(0xff0000);
899
+ setTimeout(() => {
900
+ sphereMesh.material.color.setHex(0x846c5b);
901
+ }, 200);
902
+ });
903
+ };
904
+
905
+ const groundMesh = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardMaterial({ color: 0xcccccc }));
906
+ groundMesh.rotation.x = -Math.PI / 2;
907
+ scene.add(groundMesh);
908
+
909
+ document.getElementById('drop-ball-btn').onclick = createSphere;
910
+
911
+ const debugToggle = document.getElementById('physics-debug-toggle');
912
+ debugToggle.addEventListener('change', () => {
913
+ cannonDebugRenderer.enabled = debugToggle.checked;
914
+ // If a ball is dropped when debug is off, it needs to be added to the renderer's meshes
915
+ if (debugToggle.checked) {
916
+ cannonDebugRenderer.update();
917
+ }
918
+ });
919
+
920
+ const timeStep = 1 / 60;
921
+ function animate() {
922
+ requestAnimationFrame(animate);
923
+ world.step(timeStep);
924
+ controls.update();
925
+
926
+ if (debugToggle.checked) {
927
+ cannonDebugRenderer.update();
928
+ }
929
+
930
+ for (let i = 0; i < meshes.length; i++) {
931
+ meshes[i].position.copy(bodies[i].position);
932
+ meshes[i].quaternion.copy(bodies[i].quaternion);
933
+ }
934
+
935
+ renderer.render(scene, camera);
936
+ }
937
+ animate();
938
+
939
+ window.addEventListener('resize', () => {
940
+ camera.aspect = container.clientWidth / container.clientHeight;
941
+ camera.updateProjectionMatrix();
942
+ renderer.setSize(container.clientWidth, container.clientHeight);
943
+ });
944
+ }
945
+
946
+ // --- Scene 4: Particle & Fluid Simulation ---
947
+ function initParticles() {
948
+ const container = document.getElementById('particles-canvas-container');
949
+ if (!container || container.querySelector('canvas')) return;
950
+
951
+ const scene = new THREE.Scene();
952
+ scene.background = new THREE.Color(0x000000);
953
+
954
+ const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
955
+ camera.position.set(0, 10, 20);
956
+
957
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
958
+ renderer.setSize(container.clientWidth, container.clientHeight);
959
+ container.appendChild(renderer.domElement);
960
+
961
+ const controls = new OrbitControls(camera, renderer.domElement);
962
+ controls.enableDamping = true;
963
+ controls.target.set(0, 0, 0);
964
+
965
+ const waterGeometry = new THREE.PlaneGeometry(2000, 2000);
966
+
967
+ const water = new Water(
968
+ waterGeometry,
969
+ {
970
+ textureWidth: 512,
971
+ textureHeight: 512,
972
+ waterNormals: new THREE.TextureLoader().load('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/waternormals.jpg', function (texture) {
973
+ texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
974
+ }),
975
+ sunDirection: new THREE.Vector3(0, 1, 0),
976
+ sunColor: 0x48494b,
977
+ waterColor: 0x005577,
978
+ distortionScale: 3.7,
979
+ fog: scene.fog !== undefined
980
+ }
981
+ );
982
+ water.rotation.x = -Math.PI / 2;
983
+ scene.add(water);
984
+
985
+ const particleCount = 500;
986
+ const positions = new Float32Array(particleCount * 3);
987
+ const velocities = new Float32Array(particleCount * 3);
988
+ const particleTypes = new Array(particleCount).fill(null);
989
+ const colors = new Float32Array(particleCount * 3);
990
+
991
+ const particleGeometry = new THREE.BufferGeometry();
992
+ particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
993
+ particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
994
+
995
+ const particleMaterial = new THREE.PointsMaterial({
996
+ size: 0.5,
997
+ vertexColors: true,
998
+ sizeAttenuation: true,
999
+ transparent: true,
1000
+ opacity: 0.9,
1001
+ blending: THREE.AdditiveBlending
1002
+ });
1003
+ const particleSystem = new THREE.Points(particleGeometry, particleMaterial);
1004
+ scene.add(particleSystem);
1005
+
1006
+ const woodColor = new THREE.Color(0x8B4513);
1007
+ const metalColor = new THREE.Color(0x708090);
1008
+
1009
+ let particleCursor = 0;
1010
+ const spawnParticles = (type) => {
1011
+ const count = 50;
1012
+ const spread = 20;
1013
+ const startY = 15;
1014
+
1015
+ for (let i = 0; i < count; i++) {
1016
+ const idx = particleCursor * 3;
1017
+ positions[idx] = (Math.random() - 0.5) * spread;
1018
+ positions[idx + 1] = startY + Math.random() * 5;
1019
+ positions[idx + 2] = (Math.random() - 0.5) * spread;
1020
+
1021
+ velocities[idx] = 0;
1022
+ velocities[idx + 1] = -0.3 - Math.random() * 0.3;
1023
+ velocities[idx + 2] = 0;
1024
+
1025
+ particleTypes[particleCursor] = type;
1026
+
1027
+ if (type === 'wood') {
1028
+ woodColor.toArray(colors, idx);
1029
+ } else {
1030
+ metalColor.toArray(colors, idx);
1031
+ }
1032
+
1033
+ particleCursor = (particleCursor + 1) % particleCount;
1034
+ }
1035
+ particleGeometry.attributes.position.needsUpdate = true;
1036
+ particleGeometry.attributes.color.needsUpdate = true;
1037
+ };
1038
+
1039
+ document.getElementById('drop-wood-btn').onclick = () => spawnParticles('wood');
1040
+ document.getElementById('drop-metal-btn').onclick = () => spawnParticles('metal');
1041
+
1042
+ function animate() {
1043
+ requestAnimationFrame(animate);
1044
+
1045
+ controls.update();
1046
+ water.material.uniforms['time'].value += 1.0 / 60.0;
1047
+
1048
+ for (let i = 0; i < particleCount * 3; i += 3) {
1049
+ const particleIndex = i / 3;
1050
+ const currentY = positions[i + 1];
1051
+
1052
+ if (currentY < water.position.y) {
1053
+ if (particleTypes[particleIndex] === 'wood') {
1054
+ positions[i + 1] = water.position.y;
1055
+ velocities[i + 1] = 0;
1056
+ } else if (particleTypes[particleIndex] === 'metal') {
1057
+ velocities[i + 1] *= 0.98;
1058
+ }
1059
+ } else {
1060
+ velocities[i + 1] -= 0.005;
1061
+ }
1062
+
1063
+ positions[i] += velocities[i];
1064
+ positions[i + 1] += velocities[i + 1];
1065
+ positions[i + 2] += velocities[i + 2];
1066
+ }
1067
+
1068
+ particleGeometry.attributes.position.needsUpdate = true;
1069
+ renderer.render(scene, camera);
1070
+ }
1071
+ animate();
1072
+
1073
+ window.addEventListener('resize', () => {
1074
+ camera.aspect = container.clientWidth / container.clientHeight;
1075
+ camera.updateProjectionMatrix();
1076
+ renderer.setSize(container.clientWidth, container.clientHeight);
1077
+ });
1078
+ }
1079
+
1080
+ // --- Debugging Logic ---
1081
+ const debugConsole = document.getElementById('debug-console');
1082
+ const fixDisplay = document.getElementById('debug-fix-display');
1083
+ const fixesContent = document.getElementById('fixes');
1084
+
1085
+ debugConsole.addEventListener('click', (e) => {
1086
+ const line = e.target.closest('.debug-line');
1087
+ if (line) {
1088
+ const fixId = line.dataset.fix;
1089
+ const content = fixesContent.querySelector(`#${fixId}`).innerHTML;
1090
+ fixDisplay.innerHTML = `<h3 class="text-xl font-semibold mb-2">Analysis & Fix</h3>${content}`;
1091
+ }
1092
+ });
1093
+
1094
+ // Initializer for scenes
1095
+ const observer = new MutationObserver((mutationsList) => {
1096
+ for (const mutation of mutationsList) {
1097
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
1098
+ const target = mutation.target;
1099
+ if (target.classList.contains('active')) {
1100
+ if (target.id === 'fundamentals') initFundamentals();
1101
+ if (target.id === 'models') initModels();
1102
+ if (target.id === 'physics') initPhysics();
1103
+ if (target.id === 'particles') initParticles();
1104
+ }
1105
+ }
1106
+ }
1107
+ });
1108
+
1109
+ sections.forEach(section => {
1110
+ observer.observe(section, { attributes: true });
1111
+ });
1112
+
1113
+ initFundamentals();
1114
+
1115
+ </script>
1116
+ </body>
1117
  </html>