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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +552 -617
index.html CHANGED
@@ -1,101 +1,177 @@
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);
@@ -104,9 +180,7 @@ 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
 
@@ -118,27 +192,19 @@ function animate() {
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
@@ -146,57 +212,45 @@ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
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>
@@ -204,12 +258,12 @@ animate();
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>
@@ -229,279 +283,98 @@ animate();
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;
@@ -531,25 +404,25 @@ function animate() {
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">
@@ -557,58 +430,25 @@ animate();
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>
@@ -638,71 +478,45 @@ scene.add(directionalLight);</code></pre>
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);
@@ -713,38 +527,107 @@ scene.add(directionalLight);</code></pre>
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
 
@@ -759,124 +642,135 @@ scene.add(directionalLight);</code></pre>
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 });
@@ -885,15 +779,11 @@ scene.add(directionalLight);</code></pre>
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(() => {
@@ -902,55 +792,24 @@ scene.add(directionalLight);</code></pre>
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
 
@@ -961,9 +820,8 @@ scene.add(directionalLight);</code></pre>
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
  {
@@ -1007,6 +865,7 @@ scene.add(directionalLight);</code></pre>
1007
  const metalColor = new THREE.Color(0x708090);
1008
 
1009
  let particleCursor = 0;
 
1010
  const spawnParticles = (type) => {
1011
  const count = 50;
1012
  const spread = 20;
@@ -1036,11 +895,15 @@ scene.add(directionalLight);</code></pre>
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;
@@ -1067,51 +930,123 @@ scene.add(directionalLight);</code></pre>
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>
 
1
  <!DOCTYPE html>
2
+ <html lang="en" class="bg-gray-100">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>3D Web Development Guide</title>
7
+ <!-- Use a simple color palette for a friendly, warm tone -->
8
  <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
 
 
 
 
 
 
 
9
  <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
11
+
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ }
15
+
16
+ .container {
17
+ max-width: 1200px;
18
+ }
19
+
20
+ .tab-inactive {
21
+ color: #78716c; /* Warm gray */
22
+ border-color: transparent;
23
+ }
24
+
25
+ .tab-active {
26
+ color: #654321; /* Dark brown */
27
+ border-color: #654321;
28
+ }
29
+
30
+ .interactive-canvas-container {
31
+ position: relative;
32
+ width: 100%;
33
+ padding-top: 56.25%; /* 16:9 Aspect Ratio */
34
+ border-radius: 0.5rem;
35
+ overflow: hidden;
36
+ background-color: #e7e5e4;
37
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
38
+ }
39
+
40
+ .interactive-canvas-container canvas {
41
+ position: absolute;
42
+ top: 0;
43
+ left: 0;
44
+ width: 100%;
45
+ height: 100%;
46
+ }
47
+
48
+ /* Basic Accordion Styling */
49
+ .accordion-item {
50
+ border: 1px solid #d1d5db;
51
+ border-radius: 0.5rem;
52
+ }
53
+
54
+ .accordion-button {
55
+ background-color: #f9fafb;
56
+ color: #1f2937;
57
+ }
58
+
59
+ .accordion-button:hover {
60
+ background-color: #f3f4f6;
61
+ }
62
+
63
+ .accordion-arrow {
64
+ transition: transform 0.3s ease;
65
+ }
66
+
67
+ .accordion-button.open .accordion-arrow {
68
+ transform: rotate(180deg);
69
+ }
70
+
71
+ .accordion-content {
72
+ max-height: 0;
73
+ overflow: hidden;
74
+ transition: max-height 0.3s ease;
75
+ background-color: #fff;
76
+ padding: 0 1rem;
77
+ }
78
+
79
+ .accordion-content p {
80
+ padding: 0.5rem 0;
81
+ }
82
+
83
+ .accordion-button.open + .accordion-content {
84
+ max-height: 500px; /* Adjust as needed for content */
85
+ }
86
+
87
+ /* Debug Console Styling */
88
+ .debug-console {
89
+ background-color: #1e1e1e;
90
+ color: #d4d4d4;
91
+ padding: 1rem;
92
+ border-radius: 0.5rem;
93
+ font-family: 'Consolas', 'Courier New', monospace;
94
+ white-space: pre-wrap;
95
+ line-height: 1.5;
96
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
97
+ }
98
+ .debug-info { color: #569cd6; }
99
+ .debug-warn { color: #dcdcaa; }
100
+ .debug-error { color: #f44747; }
101
+ .debug-line { padding: 0.2rem 0; cursor: pointer; }
102
+ .debug-line:hover { background-color: rgba(255, 255, 255, 0.05); }
103
+
104
  </style>
105
  </head>
106
+ <body class="bg-gray-100 text-gray-800">
107
 
108
  <div class="container mx-auto p-4 sm:p-6 lg:p-8">
109
+
110
  <header class="text-center mb-8">
111
  <h1 class="text-4xl md:text-5xl font-bold text-gray-800" style="color: #654321;">Varun Mulay's Guide to 3D Web Development</h1>
112
  <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>
113
  </header>
114
 
115
+ <nav class="mb-8 border-b border-gray-200 overflow-x-auto">
116
+ <ul class="flex flex-nowrap text-sm font-medium text-center" id="tab-nav">
117
+ <li class="flex-shrink-0">
118
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-active" id="fundamentals-tab">1. Fundamentals</button>
119
+ </li>
120
+ <li class="flex-shrink-0">
121
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="controls-tab">2. Orbital Controls</button>
122
+ </li>
123
+ <li class="flex-shrink-0">
124
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="models-tab">3. Importing Models</button>
125
+ </li>
126
+ <li class="flex-shrink-0">
127
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="physics-tab">4. Physics with Cannon.js</button>
128
+ </li>
129
+ <li class="flex-shrink-0">
130
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="particles-tab">5. Particle & Fluid Simulation</button>
131
+ </li>
132
+ <li class="flex-shrink-0">
133
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="debugging-tab">6. Debugging</button>
134
+ </li>
135
+ <li class="flex-shrink-0">
136
+ <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="model-viewer-tab">7. Model Viewer</button>
137
+ </li>
138
  </ul>
139
  </nav>
140
 
 
141
  <main id="tab-content">
142
  <!-- 1. Fundamentals -->
143
+ <section id="fundamentals" class="space-y-6 hidden">
144
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">Three.js Fundamentals: The Core Components</h2>
145
+ <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>. This section provides an interactive demonstration of these foundational elements.</p>
146
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
147
  <div>
148
+ <div class="interactive-canvas-container" id="fundamentals-canvas"></div>
149
  <div class="mt-4 flex flex-wrap gap-2 justify-center">
150
+ <button id="fundamentals-move-x" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Move X</button>
151
+ <button id="fundamentals-move-y" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Move Y</button>
152
+ <button id="fundamentals-color" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Change Color</button>
153
  </div>
154
  </div>
155
  <div>
156
  <h3 class="text-xl font-semibold mb-2">Core Logic Explained</h3>
157
+ <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.</p>
158
+ <pre><code class="language-javascript">{`
159
  // 1. Scene: The container for all objects
160
  const scene = new THREE.Scene();
161
  scene.background = new THREE.Color(0xf0f0f0);
162
 
163
  // 2. Camera: Defines the viewpoint
164
  const camera = new THREE.PerspectiveCamera(
165
+ 75, width / height, 0.1, 1000
 
 
 
166
  );
167
  camera.position.z = 5;
168
 
169
  // 3. Renderer: Renders the scene
170
  const renderer = new THREE.WebGLRenderer();
171
  renderer.setSize(width, height);
172
+ container.appendChild(renderer.domElement);
173
 
174
+ // Add lighting
175
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
176
  scene.add(ambientLight);
177
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
 
180
 
181
  // Create an object (Mesh = Geometry + Material)
182
  const geometry = new THREE.BoxGeometry(1, 1, 1);
183
+ const material = new THREE.MeshStandardMaterial({ color: 0x846c5b });
 
 
184
  const cube = new THREE.Mesh(geometry, material);
185
  scene.add(cube);
186
 
 
192
  renderer.render(scene, camera);
193
  }
194
  animate();
195
+ `}</code></pre>
 
 
 
 
 
 
 
 
196
  </div>
197
  </div>
198
  </section>
199
+
200
  <!-- 2. Orbital Controls -->
201
+ <section id="controls" class="space-y-6 hidden">
202
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">Mastering Orbital Controls</h2>
203
+ <p><code>OrbitControls</code> is a powerful tool that gives the user freedom to pan, zoom, and rotate the camera. This section explains the correct setup and provides a troubleshooting guide for common errors.</p>
204
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
205
  <div>
206
  <h3 class="text-xl font-semibold mb-2">Correct Implementation</h3>
207
+ <pre><code class="language-javascript">{`
208
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
209
 
210
  // ... After setting up scene, camera, renderer
 
212
  // Initialize controls
213
  const controls = new OrbitControls(camera, renderer.domElement);
214
 
 
 
 
 
 
 
 
 
215
  // CRITICAL: Update controls in the animation loop
216
  function animate() {
217
  requestAnimationFrame(animate);
 
 
218
  controls.update();
 
219
  renderer.render(scene, camera);
220
  }
 
221
  animate();
222
+ `}</code></pre>
223
  </div>
224
  <div class="space-y-4">
225
  <h3 class="text-xl font-semibold mb-2">Troubleshooting Common Errors</h3>
226
+ <div class="accordion-item">
227
  <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
228
  <span>My controls feel laggy or don't stop immediately.</span>
229
  <span class="accordion-arrow transform transition-transform">▼</span>
230
  </button>
231
  <div class="accordion-content px-4 pb-4">
232
  <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>
233
+ <p><strong>Fix:</strong> Add <code>controls.update()</code> to your <code>animate</code> function.</p>
234
  </div>
235
  </div>
236
+ <div class="accordion-item">
237
  <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
238
  <span>The camera zooms in/out but doesn't rotate.</span>
239
  <span class="accordion-arrow transform transition-transform">▼</span>
240
  </button>
241
  <div class="accordion-content px-4 pb-4">
242
+ <p><strong>Cause:</strong> The OrbitControls script has not loaded correctly.</p>
243
+ <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.</p>
244
  </div>
245
  </div>
246
+ <div class="accordion-item">
247
  <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
248
  <span>My camera can go below the "ground" plane.</span>
249
  <span class="accordion-arrow transform transition-transform">▼</span>
250
  </button>
251
  <div class="accordion-content px-4 pb-4">
252
  <p><strong>Cause:</strong> The default polar angle allows for 360-degree vertical rotation.</p>
253
+ <p><strong>Fix:</strong> Limit the vertical rotation by setting <code>controls.maxPolarAngle = Math.PI / 2;</code></p>
254
  </div>
255
  </div>
256
  </div>
 
258
  </section>
259
 
260
  <!-- 3. Importing Models -->
261
+ <section id="models" class="space-y-6 hidden">
262
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">Importing 3D Models (.glb/.gltf)</h2>
263
+ <p>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>
264
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
265
  <div>
266
+ <div class="interactive-canvas-container" id="models-canvas"></div>
267
  <div class="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4 p-4 bg-white border rounded-md">
268
  <div>
269
  <label for="model-pos-x" class="block text-sm font-medium">Position X</label>
 
283
  Upload Model (.glb/.gltf)
284
  <input type="file" id="model-upload" class="hidden" accept=".glb, .gltf">
285
  </label>
 
286
  </div>
287
  </div>
288
  <div>
289
  <h3 class="text-xl font-semibold mb-2">Model Loader & Transformation Code</h3>
290
+ <p class="mb-4">Loading models is an asynchronous operation. You use a specific loader and define a callback function that executes once the model has loaded successfully.</p>
291
+ <pre><code class="language-javascript">{`
292
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
293
 
294
  const loader = new GLTFLoader();
295
+ loader.load(
296
+ 'path/to/model.glb',
297
+ function (gltf) {
298
+ const model = gltf.scene;
299
+ // Transformations applied here
300
+ model.position.set(0, -1, 0);
301
+ model.scale.set(1.5, 1.5, 1.5);
302
+ scene.add(model);
303
+ },
304
+ undefined,
305
+ function (error) {
306
+ console.error('An error occurred while loading the model:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  }
308
+ );
309
+ `}</code></pre>
310
  </div>
311
  </div>
312
  </section>
313
 
314
  <!-- 4. Physics with Cannon.js -->
315
+ <section id="physics" class="space-y-6 hidden">
316
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">The Physics Engine: Cannon.js</h2>
317
+ <p>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. Click "Drop Ball" to see the collision event.</p>
318
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
319
  <div>
320
+ <div class="interactive-canvas-container" id="physics-canvas"></div>
321
  <div class="mt-4 flex flex-wrap gap-2 justify-center">
322
+ <button id="drop-ball" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Ball</button>
323
  <label class="flex items-center space-x-2 px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm">
324
+ <input type="checkbox" id="physics-debug" class="form-checkbox h-4 w-4 text-gray-600 transition-colors">
325
  <span class="text-sm font-medium text-gray-700">Debug Physics</span>
326
  </label>
327
  </div>
328
  </div>
329
  <div>
330
  <h3 class="text-xl font-semibold mb-2">Connecting Physics to Rendering</h3>
331
+ <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.</p>
332
+ <pre><code class="language-javascript">{`
333
  import * as CANNON from 'cannon-es';
334
  import * as CannonDebugRenderer from 'cannon-es-debugger';
335
 
336
+ const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
 
 
 
 
 
 
 
337
  const cannonDebugRenderer = new CannonDebugRenderer.default(scene, world);
338
 
339
+ // Create the physical ground plane
340
+ const groundBody = new CANNON.Body({ mass: 0 });
 
341
  groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
342
  world.addBody(groundBody);
343
 
344
+ // Update both in the animation loop
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  function animate() {
346
  requestAnimationFrame(animate);
 
 
347
  world.fixedStep();
 
 
348
  cannonDebugRenderer.update();
 
349
  // Sync each Three.js mesh with its Cannon.js body
 
 
350
  renderer.render(scene, camera);
351
  }
352
  animate();
353
+ `}</code></pre>
 
 
 
 
 
 
 
 
 
 
 
354
  </div>
355
  </div>
356
  </section>
357
+
358
  <!-- 5. Particle & Fluid Simulation -->
359
+ <section id="particles" class="space-y-6 hidden">
360
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">Buoyancy and Density Simulation</h2>
361
+ <p>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.</p>
362
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
363
  <div>
364
+ <div class="interactive-canvas-container" id="particles-canvas"></div>
365
  <div class="mt-4 flex flex-wrap gap-2 justify-center">
366
+ <button id="drop-wood" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Wood</button>
367
+ <button id="drop-metal" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Metal</button>
368
  </div>
369
  </div>
370
  <div>
371
  <h3 class="text-xl font-semibold mb-2">Simulation Logic</h3>
372
+ <p class="mb-4">Here's a breakdown of the code that creates the simulation. The animation loop applies a different "physics" rule to each particle once it hits the water plane.</p>
373
+ <pre><code class="language-javascript">{`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  // The main animation loop
375
  function animate() {
376
  requestAnimationFrame(animate);
377
 
 
 
 
378
  // Update particle positions and apply physics
379
  for (let i = 0; i < particleCount * 3; i += 3) {
380
  const particleIndex = i / 3;
 
404
  renderer.render(scene, camera);
405
  }
406
  animate();
407
+ `}</code></pre>
408
  </div>
409
  </div>
410
  </section>
411
 
412
  <!-- 6. Debugging -->
413
+ <section id="debugging" class="space-y-6 hidden">
414
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">A Game Developer's Debugging Toolkit</h2>
415
+ <p>When things go wrong in 3D, the errors can be cryptic. A blank screen is a common symptom for many different problems. 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>
416
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
417
  <div class="space-y-4">
418
  <h3 class="text-xl font-semibold mb-2">Interactive Debug Console</h3>
419
  <div class="debug-console" id="debug-console">
420
+ <div class="debug-line debug-info" data-fix="fix1">▷ INFO: THREE.WebGLRenderer 164</div>
421
+ <div class="debug-line debug-warn" data-fix="fix2">▷ WARN: Scene has no lights. Objects may appear black.</div>
422
+ <div class="debug-line debug-error" data-fix="fix3">▷ ERROR: TypeError: Cannot read properties of undefined (reading 'scene')</div>
423
+ <div class="debug-line debug-error" data-fix="fix4">▷ ERROR: Failed to load resource: net::ERR_FILE_NOT_FOUND Horse.glb</div>
424
+ <div class="debug-line debug-info" data-fix="fix5">▷ INFO: Animation loop started.</div>
425
+ <div class="debug-line debug-error" data-fix="fix6">▷ ERROR: Uncaught TypeError: Cannot set properties of null (setting 'position')</div>
426
  </div>
427
  </div>
428
  <div class="p-4 bg-white border rounded-md" id="debug-fix-display">
 
430
  <p class="text-gray-500">Click on an error in the console to see a detailed explanation and solution here.</p>
431
  </div>
432
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  </section>
434
 
435
  <!-- 7. Model Viewer -->
436
+ <section id="model-viewer" class="space-y-6 hidden">
437
  <h2 class="text-3xl font-bold" style="color: #a58d6f;">The Easy Way: Google's &lt;model-viewer&gt;</h2>
438
+ <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.</p>
439
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
440
  <div>
441
  <h3 class="text-xl font-semibold mb-2">Live Example</h3>
442
+ <div id="model-viewer-container">
443
+ <model-viewer
444
+ src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
445
+ alt="A 3D model of an astronaut"
446
+ ar
447
+ auto-rotate
448
+ camera-controls
449
+ style="width: 100%; height: 400px; background-color: #f0f0f0; border-radius: 8px;"
450
+ ></model-viewer>
451
+ </div>
452
  </div>
453
  <div>
454
  <h3 class="text-xl font-semibold mb-2">When to Use Which?</h3>
 
478
  </main>
479
  </div>
480
 
 
 
 
481
  <script type="module">
482
+ import * as THREE from 'https://cdn.skypack.dev/three@0.132.2';
483
+ import { OrbitControls } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/controls/OrbitControls.js';
484
+ import { GLTFLoader } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/loaders/GLTFLoader.js';
485
+ import * as CANNON from 'https://cdn.skypack.dev/cannon-es@0.20.0';
486
+ import CannonDebugger from 'https://cdn.skypack.dev/cannon-es-debugger@1.0.0';
487
+ import { Water } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/objects/Water.js';
488
+
489
+ const tabs = document.querySelectorAll('#tab-nav button');
490
+ const sections = document.querySelectorAll('#tab-content section');
491
+ const disposers = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
+ // Function to clean up a scene
494
+ const cleanupScene = (canvasId) => {
495
+ const canvasContainer = document.getElementById(canvasId);
496
+ if (!canvasContainer) return;
497
+ const canvas = canvasContainer.querySelector('canvas');
498
+ if (canvas) {
499
+ if (disposers[canvasId]) {
500
+ disposers[canvasId]();
501
+ delete disposers[canvasId];
 
 
 
502
  }
503
+ canvas.parentNode.removeChild(canvas);
504
+ }
505
+ };
506
 
507
+ // Function to set up the fundamentals scene
508
+ const setupFundamentals = () => {
509
+ const container = document.getElementById('fundamentals-canvas');
510
+ if (!container) return;
511
 
512
  const scene = new THREE.Scene();
513
  scene.background = new THREE.Color(0xe7e5e4);
 
514
  const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
 
 
515
  const renderer = new THREE.WebGLRenderer({ antialias: true });
516
+
517
  renderer.setSize(container.clientWidth, container.clientHeight);
518
  container.appendChild(renderer.domElement);
519
+
520
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
521
  scene.add(ambientLight);
522
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
 
527
  const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
528
  const cube = new THREE.Mesh(geometry, material);
529
  scene.add(cube);
530
+ camera.position.z = 3;
531
 
532
+ const handleResize = () => {
533
+ camera.aspect = container.clientWidth / container.clientHeight;
534
+ camera.updateProjectionMatrix();
535
+ renderer.setSize(container.clientWidth, container.clientHeight);
536
+ };
537
 
538
+ let animationFrameId;
539
+ const animate = () => {
540
+ animationFrameId = requestAnimationFrame(animate);
541
  cube.rotation.x += 0.005;
542
  cube.rotation.y += 0.005;
543
  renderer.render(scene, camera);
544
+ };
545
+
546
  animate();
547
+ window.addEventListener('resize', handleResize);
548
 
549
+ document.getElementById('fundamentals-move-x').onclick = () => {
550
+ cube.position.x += 0.5;
551
+ };
552
+ document.getElementById('fundamentals-move-y').onclick = () => {
553
+ cube.position.y += 0.5;
554
+ };
555
+ document.getElementById('fundamentals-color').onclick = () => {
556
+ cube.material.color.setHex(Math.random() * 0xffffff);
557
+ };
558
+
559
+ disposers['fundamentals-canvas'] = () => {
560
+ window.removeEventListener('resize', handleResize);
561
+ cancelAnimationFrame(animationFrameId);
562
+ scene.children.forEach(child => scene.remove(child));
563
+ renderer.dispose();
564
+ };
565
+ };
566
+
567
+ // Function to set up the controls scene
568
+ const setupControls = () => {
569
+ const container = document.getElementById('controls-canvas');
570
+ if (!container) return;
571
+
572
+ const scene = new THREE.Scene();
573
+ scene.background = new THREE.Color(0xe7e5e4);
574
+ const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
575
+ camera.position.z = 5;
576
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
577
+
578
+ renderer.setSize(container.clientWidth, container.clientHeight);
579
+ container.appendChild(renderer.domElement);
580
+
581
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
582
+ scene.add(ambientLight);
583
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
584
+ directionalLight.position.set(5, 5, 5);
585
+ scene.add(directionalLight);
586
+
587
+ const geometry = new THREE.BoxGeometry(1, 1, 1);
588
+ const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
589
+ const cube = new THREE.Mesh(geometry, material);
590
+ scene.add(cube);
591
+
592
+ const controls = new OrbitControls(camera, renderer.domElement);
593
+ controls.enableDamping = true; // an animation loop is required when damping is enabled
594
+ controls.dampingFactor = 0.05;
595
+
596
+ const handleResize = () => {
597
  camera.aspect = container.clientWidth / container.clientHeight;
598
  camera.updateProjectionMatrix();
599
  renderer.setSize(container.clientWidth, container.clientHeight);
600
+ };
601
+
602
+ let animationFrameId;
603
+ const animate = () => {
604
+ animationFrameId = requestAnimationFrame(animate);
605
+ controls.update();
606
+ renderer.render(scene, camera);
607
+ };
608
+
609
+ animate();
610
+ window.addEventListener('resize', handleResize);
611
+
612
+ disposers['controls-canvas'] = () => {
613
+ window.removeEventListener('resize', handleResize);
614
+ cancelAnimationFrame(animationFrameId);
615
+ scene.children.forEach(child => scene.remove(child));
616
+ renderer.dispose();
617
+ };
618
+ };
619
+
620
+ // Function to set up the models scene
621
+ const setupModels = () => {
622
+ const container = document.getElementById('models-canvas');
623
+ if (!container) return;
624
 
625
  const scene = new THREE.Scene();
626
  scene.background = new THREE.Color(0xe7e5e4);
 
627
  const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
628
  camera.position.set(0, 1.5, 4);
 
629
  const renderer = new THREE.WebGLRenderer({ antialias: true });
630
+
631
  renderer.setSize(container.clientWidth, container.clientHeight);
632
  container.appendChild(renderer.domElement);
633
 
 
642
  scene.add(dirLight);
643
 
644
  const loader = new GLTFLoader();
645
+ let model;
646
+ loader.load('https://cdn.jsdelivr.net/npm/three@0.164.1/examples/models/gltf/Horse.glb', (gltf) => {
647
+ model = gltf.scene;
648
+ model.position.y = -1;
649
+ model.scale.set(1.5, 1.5, 1.5);
650
+ scene.add(model);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  });
652
 
653
+ const handleResize = () => {
654
+ camera.aspect = container.clientWidth / container.clientHeight;
655
+ camera.updateProjectionMatrix();
656
+ renderer.setSize(container.clientWidth, container.clientHeight);
657
+ };
658
 
659
+ let animationFrameId;
660
+ const animate = () => {
661
+ animationFrameId = requestAnimationFrame(animate);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  controls.update();
663
  renderer.render(scene, camera);
664
+ };
 
665
 
666
+ animate();
667
+ window.addEventListener('resize', handleResize);
668
+
669
+ document.getElementById('model-pos-x').oninput = (e) => {
670
+ if (model) model.position.x = parseFloat(e.target.value);
671
+ };
672
+ document.getElementById('model-rot-y').oninput = (e) => {
673
+ if (model) model.rotation.y = parseFloat(e.target.value);
674
+ };
675
+ document.getElementById('model-scale').oninput = (e) => {
676
+ if (model) model.scale.set(parseFloat(e.target.value), parseFloat(e.target.value), parseFloat(e.target.value));
677
+ };
678
+ document.getElementById('model-upload').onchange = (e) => {
679
+ const file = e.target.files[0];
680
+ if (!file) return;
681
+
682
+ const url = URL.createObjectURL(file);
683
+ loader.load(url, (gltf) => {
684
+ if (model) scene.remove(model);
685
+ model = gltf.scene;
686
+ model.position.y = -1;
687
+ scene.add(model);
688
+ });
689
+ };
690
+
691
+ disposers['models-canvas'] = () => {
692
+ window.removeEventListener('resize', handleResize);
693
+ cancelAnimationFrame(animationFrameId);
694
+ scene.children.forEach(child => scene.remove(child));
695
+ renderer.dispose();
696
+ };
697
+ };
698
+
699
+ // Function to set up the physics scene
700
+ const setupPhysics = () => {
701
+ const container = document.getElementById('physics-canvas');
702
+ if (!container) return;
703
 
 
 
 
 
 
704
  const scene = new THREE.Scene();
705
  scene.background = new THREE.Color(0xe7e5e4);
 
706
  const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
707
  camera.position.set(0, 5, 10);
 
708
  const renderer = new THREE.WebGLRenderer({ antialias: true });
709
+
710
  renderer.setSize(container.clientWidth, container.clientHeight);
711
  container.appendChild(renderer.domElement);
712
 
713
  const controls = new OrbitControls(camera, renderer.domElement);
714
  controls.enableDamping = true;
715
+
716
  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
717
  scene.add(ambientLight);
718
  const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
719
  directionalLight.position.set(5, 10, 7.5);
720
  scene.add(directionalLight);
721
+
722
+ const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
 
 
 
 
 
 
723
  const groundShape = new CANNON.Plane();
724
+ const groundBody = new CANNON.Body({ mass: 0 });
725
+ groundBody.addShape(groundShape);
726
  groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
727
  world.addBody(groundBody);
728
 
729
+ const groundMesh = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardMaterial({ color: 0xcccccc }));
730
+ groundMesh.rotation.x = -Math.PI / 2;
731
+ scene.add(groundMesh);
732
+
733
  const meshes = [];
734
  const bodies = [];
735
+ let debuggerInstance;
736
+
737
+ const handleResize = () => {
738
+ camera.aspect = container.clientWidth / container.clientHeight;
739
+ camera.updateProjectionMatrix();
740
+ renderer.setSize(container.clientWidth, container.clientHeight);
741
+ };
742
+
743
+ let animationFrameId;
744
+ const timeStep = 1 / 60;
745
+ const animate = () => {
746
+ animationFrameId = requestAnimationFrame(animate);
747
+ world.step(timeStep);
748
+ controls.update();
749
 
750
+ if (document.getElementById('physics-debug').checked) {
751
+ if (!debuggerInstance) {
752
+ debuggerInstance = new CannonDebugger(scene, world);
753
+ }
754
+ debuggerInstance.update();
755
+ } else {
756
+ if (debuggerInstance) {
757
+ debuggerInstance.destroy();
758
+ debuggerInstance = null;
759
+ }
760
+ }
761
+
762
+ for (let i = 0; i < meshes.length; i++) {
763
+ meshes[i].position.copy(bodies[i].position);
764
+ meshes[i].quaternion.copy(bodies[i].quaternion);
765
+ }
766
+
767
+ renderer.render(scene, camera);
768
+ };
769
+
770
+ animate();
771
+ window.addEventListener('resize', handleResize);
772
+
773
+ document.getElementById('drop-ball').onclick = () => {
774
  const radius = 0.5;
775
  const sphereGeometry = new THREE.SphereGeometry(radius, 32, 32);
776
  const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0x846c5b });
 
779
  scene.add(sphereMesh);
780
  meshes.push(sphereMesh);
781
 
782
+ const sphereBody = new CANNON.Body({ mass: 1, shape: new CANNON.Sphere(radius) });
 
 
 
783
  sphereBody.position.y = 10;
784
  world.addBody(sphereBody);
785
  bodies.push(sphereBody);
786
 
 
787
  sphereBody.addEventListener('collide', (event) => {
788
  sphereMesh.material.color.setHex(0xff0000);
789
  setTimeout(() => {
 
792
  });
793
  };
794
 
795
+ disposers['physics-canvas'] = () => {
796
+ window.removeEventListener('resize', handleResize);
797
+ cancelAnimationFrame(animationFrameId);
798
+ if (debuggerInstance) { debuggerInstance.destroy(); }
799
+ scene.children.forEach(child => scene.remove(child));
800
+ renderer.dispose();
801
+ meshes.length = 0;
802
+ bodies.length = 0;
803
+ };
804
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
806
+ // Function to set up the particle simulation
807
+ const setupParticles = () => {
808
+ const container = document.getElementById('particles-canvas');
809
+ if (!container) return;
 
 
 
 
 
 
 
 
 
 
 
810
 
811
  const scene = new THREE.Scene();
812
  scene.background = new THREE.Color(0x000000);
 
813
  const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
814
  camera.position.set(0, 10, 20);
815
 
 
820
  const controls = new OrbitControls(camera, renderer.domElement);
821
  controls.enableDamping = true;
822
  controls.target.set(0, 0, 0);
823
+
824
  const waterGeometry = new THREE.PlaneGeometry(2000, 2000);
 
825
  const water = new Water(
826
  waterGeometry,
827
  {
 
865
  const metalColor = new THREE.Color(0x708090);
866
 
867
  let particleCursor = 0;
868
+
869
  const spawnParticles = (type) => {
870
  const count = 50;
871
  const spread = 20;
 
895
  particleGeometry.attributes.color.needsUpdate = true;
896
  };
897
 
898
+ const handleResize = () => {
899
+ camera.aspect = container.clientWidth / container.clientHeight;
900
+ camera.updateProjectionMatrix();
901
+ renderer.setSize(container.clientWidth, container.clientHeight);
902
+ };
903
 
904
+ let animationFrameId;
905
+ const animate = () => {
906
+ animationFrameId = requestAnimationFrame(animate);
907
 
908
  controls.update();
909
  water.material.uniforms['time'].value += 1.0 / 60.0;
 
930
 
931
  particleGeometry.attributes.position.needsUpdate = true;
932
  renderer.render(scene, camera);
933
+ };
934
+
935
  animate();
936
+ window.addEventListener('resize', handleResize);
937
 
938
+ document.getElementById('drop-wood').onclick = () => spawnParticles('wood');
939
+ document.getElementById('drop-metal').onclick = () => spawnParticles('metal');
940
+
941
+ disposers['particles-canvas'] = () => {
942
+ window.removeEventListener('resize', handleResize);
943
+ cancelAnimationFrame(animationFrameId);
944
+ scene.children.forEach(child => scene.remove(child));
945
+ renderer.dispose();
946
+ };
947
+ };
948
+
949
+ const setupDebugging = () => {
950
+ const fixes = {
951
+ fix1: "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.",
952
+ fix2: "Cause: Most materials, like MeshStandardMaterial or MeshPhongMaterial, require light to be visible. Without any lights in the scene, objects using these materials will render as black. Fix: Add at least one light to your scene. An AmbientLight provides basic, flat illumination, while a DirectionalLight simulates a distant light source like the sun.",
953
+ fix3: "Cause: This typically happens when your model loader's callback function returns, but the model data is not what you expect. Your code tries to access gltf.scene, but gltf is undefined. Fix: 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.",
954
+ fix4: "Cause: The path provided to the model loader is incorrect. The browser cannot find the file at that location. Fix: 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.",
955
+ fix5: "This is a useful debugging message you can add yourself with console.log() to confirm that your animate 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 animate() to start the loop.",
956
+ fix6: "Cause: You are trying to use a variable before the object it represents has been fully created and added to the scene. Fix: 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 if (cube) { ... }.",
957
+ };
958
+ const display = document.getElementById('debug-fix-display');
959
+ const displayTitle = display.querySelector('h3');
960
+ const displayContent = display.querySelector('p');
961
+
962
+ document.querySelectorAll('.debug-line').forEach(line => {
963
+ line.onclick = (e) => {
964
+ const fixKey = e.currentTarget.getAttribute('data-fix');
965
+ const titleText = e.currentTarget.textContent.replace('▷ ', '');
966
+ displayTitle.textContent = "Analysis & Fix";
967
+ displayContent.innerHTML = `<strong>${titleText}</strong><br>${fixes[fixKey]}`;
968
+ };
969
  });
 
970
 
971
+ disposers['debugging'] = () => {
972
+ document.querySelectorAll('.debug-line').forEach(line => line.onclick = null);
973
+ displayContent.innerHTML = "Click on an error in the console to see a detailed explanation and solution here.";
974
+ };
975
+ };
976
+
977
+ // Function to set up the model viewer tab
978
+ const setupModelViewer = () => {
979
+ const script = document.createElement('script');
980
+ script.type = 'module';
981
+ script.src = 'https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js';
982
+ document.head.appendChild(script);
983
+
984
+ disposers['model-viewer'] = () => {
985
+ // No cleanup for this simple setup, as the component manages itself
986
+ };
987
+ };
988
+
989
+ const tabSetupFunctions = {
990
+ 'fundamentals': setupFundamentals,
991
+ 'controls': setupControls,
992
+ 'models': setupModels,
993
+ 'physics': setupPhysics,
994
+ 'particles': setupParticles,
995
+ 'debugging': setupDebugging,
996
+ 'model-viewer': setupModelViewer,
997
+ };
998
+
999
+ const switchTab = (tabId) => {
1000
+ // Cleanup the current scene if one exists
1001
+ sections.forEach(section => {
1002
+ if (!section.classList.contains('hidden')) {
1003
+ const currentTabId = section.id;
1004
+ if (disposers[currentTabId]) {
1005
+ disposers[currentTabId]();
1006
  }
1007
  }
1008
+ });
1009
+
1010
+ // Hide all sections and deactivate all tabs
1011
+ sections.forEach(section => section.classList.add('hidden'));
1012
+ tabs.forEach(tab => tab.classList.remove('tab-active'));
1013
+ tabs.forEach(tab => tab.classList.add('tab-inactive'));
1014
+
1015
+ // Show the selected section and activate its tab
1016
+ document.getElementById(tabId).classList.remove('hidden');
1017
+ document.getElementById(tabId + '-tab').classList.remove('tab-inactive');
1018
+ document.getElementById(tabId + '-tab').classList.add('tab-active');
1019
+
1020
+ // Run the setup function for the new tab
1021
+ if (tabSetupFunctions[tabId]) {
1022
+ tabSetupFunctions[tabId]();
1023
  }
1024
+ };
1025
+
1026
+ // Add event listeners for tab clicks
1027
+ tabs.forEach(tab => {
1028
+ tab.addEventListener('click', (e) => {
1029
+ const tabId = e.target.id.replace('-tab', '');
1030
+ switchTab(tabId);
1031
+ });
1032
  });
1033
 
1034
+ document.querySelectorAll('.accordion-button').forEach(button => {
1035
+ button.addEventListener('click', (e) => {
1036
+ const content = e.currentTarget.nextElementSibling;
1037
+ e.currentTarget.classList.toggle('open');
1038
+ if (content.style.maxHeight) {
1039
+ content.style.maxHeight = null;
1040
+ } else {
1041
+ content.style.maxHeight = content.scrollHeight + "px";
1042
+ }
1043
+ });
1044
  });
 
 
1045
 
1046
+ // Initial setup for the first tab
1047
+ window.onload = () => {
1048
+ switchTab('fundamentals');
1049
+ };
1050
  </script>
1051
  </body>
1052
  </html>