Spaces:
Running
Running
Update index.html
Browse files- 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>
|
|
|
|
| 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 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
</style>
|
| 38 |
</head>
|
| 39 |
-
<body class="
|
| 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 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
<li
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
<li
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
</ul>
|
| 59 |
</nav>
|
| 60 |
|
| 61 |
-
<!-- Content Sections -->
|
| 62 |
<main id="tab-content">
|
| 63 |
<!-- 1. Fundamentals -->
|
| 64 |
-
<section id="fundamentals" class="
|
| 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>.
|
| 67 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
| 68 |
<div>
|
| 69 |
-
<div
|
| 70 |
<div class="mt-4 flex flex-wrap gap-2 justify-center">
|
| 71 |
-
<button id="move-x
|
| 72 |
-
<button id="move-y
|
| 73 |
-
<button id="
|
| 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
|
| 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,
|
| 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 |
-
|
| 97 |
|
| 98 |
-
// Add lighting
|
| 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="
|
| 136 |
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Mastering Orbital Controls</h2>
|
| 137 |
-
<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
|
| 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
|
| 180 |
</div>
|
| 181 |
</div>
|
| 182 |
-
<div class="accordion-item
|
| 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
|
| 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
|
| 190 |
</div>
|
| 191 |
</div>
|
| 192 |
-
<div class="accordion-item
|
| 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
|
| 200 |
</div>
|
| 201 |
</div>
|
| 202 |
</div>
|
|
@@ -204,12 +258,12 @@ animate();
|
|
| 204 |
</section>
|
| 205 |
|
| 206 |
<!-- 3. Importing Models -->
|
| 207 |
-
<section id="models" class="
|
| 208 |
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Importing 3D Models (.glb/.gltf)</h2>
|
| 209 |
-
|
| 210 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
| 211 |
<div>
|
| 212 |
-
<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
|
| 238 |
-
<pre><code
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 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="
|
| 304 |
<h2 class="text-3xl font-bold" style="color: #a58d6f;">The Physics Engine: Cannon.js</h2>
|
| 305 |
-
<p>
|
| 306 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
| 307 |
<div>
|
| 308 |
-
<div
|
| 309 |
<div class="mt-4 flex flex-wrap gap-2 justify-center">
|
| 310 |
-
<button id="drop-ball
|
| 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
|
| 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
|
| 320 |
-
<pre><code class="language-javascript">
|
| 321 |
import * as CANNON from 'cannon-es';
|
| 322 |
import * as CannonDebugRenderer from 'cannon-es-debugger';
|
| 323 |
|
| 324 |
-
|
| 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 |
-
//
|
| 335 |
-
const
|
| 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 |
-
//
|
| 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="
|
| 399 |
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Buoyancy and Density Simulation</h2>
|
| 400 |
-
<p>
|
| 401 |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
| 402 |
<div>
|
| 403 |
-
<div
|
| 404 |
<div class="mt-4 flex flex-wrap gap-2 justify-center">
|
| 405 |
-
<button id="drop-wood
|
| 406 |
-
<button id="drop-metal
|
| 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
|
| 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="
|
| 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.
|
| 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
|
| 548 |
-
<div class="debug-line debug-warn
|
| 549 |
-
<div class="debug-line debug-error
|
| 550 |
-
<div class="debug-line debug-error
|
| 551 |
-
<div class="debug-line debug-info
|
| 552 |
-
<div class="debug-line debug-error
|
| 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="
|
| 599 |
<h2 class="text-3xl font-bold" style="color: #a58d6f;">The Easy Way: Google's <model-viewer></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><model-viewer></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
|
| 601 |
-
|
| 602 |
<div>
|
| 603 |
<h3 class="text-xl font-semibold mb-2">Live Example</h3>
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
|
|
|
|
|
|
| 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/
|
| 647 |
-
import { GLTFLoader } from 'three/
|
| 648 |
-
import * as CANNON from 'cannon-es';
|
| 649 |
-
import
|
| 650 |
-
import { Water } from 'three/
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
const
|
| 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 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
content.style.maxHeight = null;
|
| 685 |
-
} else {
|
| 686 |
-
content.style.maxHeight = content.scrollHeight + "px";
|
| 687 |
}
|
| 688 |
-
|
| 689 |
-
|
|
|
|
| 690 |
|
| 691 |
-
//
|
| 692 |
-
|
| 693 |
-
const container = document.getElementById('fundamentals-canvas
|
| 694 |
-
if (!container
|
| 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 |
-
|
| 718 |
-
|
| 719 |
-
|
|
|
|
|
|
|
| 720 |
|
| 721 |
-
|
| 722 |
-
|
|
|
|
| 723 |
cube.rotation.x += 0.005;
|
| 724 |
cube.rotation.y += 0.005;
|
| 725 |
renderer.render(scene, camera);
|
| 726 |
-
}
|
|
|
|
| 727 |
animate();
|
|
|
|
| 728 |
|
| 729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
camera.aspect = container.clientWidth / container.clientHeight;
|
| 731 |
camera.updateProjectionMatrix();
|
| 732 |
renderer.setSize(container.clientWidth, container.clientHeight);
|
| 733 |
-
}
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 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 |
-
|
| 807 |
-
|
|
|
|
|
|
|
|
|
|
| 808 |
|
| 809 |
-
|
| 810 |
-
const
|
| 811 |
-
|
| 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 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 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 |
-
|
| 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
|
|
|
|
| 873 |
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
|
| 874 |
world.addBody(groundBody);
|
| 875 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
const meshes = [];
|
| 877 |
const bodies = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 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 |
-
|
| 936 |
-
|
| 937 |
-
|
| 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 |
-
|
| 1040 |
-
|
|
|
|
|
|
|
|
|
|
| 1041 |
|
| 1042 |
-
|
| 1043 |
-
|
|
|
|
| 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 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
});
|
| 1078 |
-
}
|
| 1079 |
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1104 |
}
|
| 1105 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
});
|
| 1108 |
|
| 1109 |
-
|
| 1110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <model-viewer></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><model-viewer></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>
|