3jsguide / index.html
varunv2004's picture
Update index.html
6f466be verified
<!DOCTYPE html>
<html lang="en" class="bg-gray-100">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Web Development Guide</title>
<!-- Use a simple color palette for a friendly, warm tone -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
body {
font-family: 'Inter', sans-serif;
}
.container {
max-width: 1200px;
}
.tab-inactive {
color: #78716c; /* Warm gray */
border-color: transparent;
}
.tab-active {
color: #654321; /* Dark brown */
border-color: #654321;
}
.interactive-canvas-container {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 Aspect Ratio */
border-radius: 0.5rem;
overflow: hidden;
background-color: #e7e5e4;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.interactive-canvas-container canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Basic Accordion Styling */
.accordion-item {
border: 1px solid #d1d5db;
border-radius: 0.5rem;
}
.accordion-button {
background-color: #f9fafb;
color: #1f2937;
}
.accordion-button:hover {
background-color: #f3f4f6;
}
.accordion-arrow {
transition: transform 0.3s ease;
}
.accordion-button.open .accordion-arrow {
transform: rotate(180deg);
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background-color: #fff;
padding: 0 1rem;
}
.accordion-content p {
padding: 0.5rem 0;
}
.accordion-button.open + .accordion-content {
max-height: 500px; /* Adjust as needed for content */
}
/* Debug Console Styling */
.debug-console {
background-color: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 0.5rem;
font-family: 'Consolas', 'Courier New', monospace;
white-space: pre-wrap;
line-height: 1.5;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
}
.debug-info { color: #569cd6; }
.debug-warn { color: #dcdcaa; }
.debug-error { color: #f44747; }
.debug-line { padding: 0.2rem 0; cursor: pointer; }
.debug-line:hover { background-color: rgba(255, 255, 255, 0.05); }
</style>
</head>
<body class="bg-gray-100 text-gray-800">
<div class="container mx-auto p-4 sm:p-6 lg:p-8">
<header class="text-center mb-8">
<h1 class="text-4xl md:text-5xl font-bold text-gray-800" style="color: #654321;">Varun Mulay's Guide to 3D Web Development</h1>
<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>
</header>
<nav class="mb-8 border-b border-gray-200 overflow-x-auto">
<ul class="flex flex-nowrap text-sm font-medium text-center" id="tab-nav">
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-active" id="fundamentals-tab">1. Fundamentals</button>
</li>
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="controls-tab">2. Orbital Controls</button>
</li>
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="models-tab">3. Importing Models</button>
</li>
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="physics-tab">4. Physics with Cannon.js</button>
</li>
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="particles-tab">5. Particle & Fluid Simulation</button>
</li>
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="debugging-tab">6. Debugging</button>
</li>
<li class="flex-shrink-0">
<button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="model-viewer-tab">7. Model Viewer</button>
</li>
</ul>
</nav>
<main id="tab-content">
<!-- 1. Fundamentals -->
<section id="fundamentals" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Three.js Fundamentals: The Core Components</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div>
<div class="interactive-canvas-container" id="fundamentals-canvas"></div>
<div class="mt-4 flex flex-wrap gap-2 justify-center">
<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>
<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>
<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>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Core Logic Explained</h3>
<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>
<pre><code class="language-javascript">{`
// 1. Scene: The container for all objects
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
// 2. Camera: Defines the viewpoint
const camera = new THREE.PerspectiveCamera(
75, width / height, 0.1, 1000
);
camera.position.z = 5;
// 3. Renderer: Renders the scene
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
container.appendChild(renderer.domElement);
// Add lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// Create an object (Mesh = Geometry + Material)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x846c5b });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// Animation loop
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
`}</code></pre>
</div>
</div>
</section>
<!-- 2. Orbital Controls -->
<section id="controls" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Mastering Orbital Controls</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div>
<h3 class="text-xl font-semibold mb-2">Correct Implementation</h3>
<pre><code class="language-javascript">{`
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// ... After setting up scene, camera, renderer
// Initialize controls
const controls = new OrbitControls(camera, renderer.domElement);
// CRITICAL: Update controls in the animation loop
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
`}</code></pre>
</div>
<div class="space-y-4">
<h3 class="text-xl font-semibold mb-2">Troubleshooting Common Errors</h3>
<div class="accordion-item">
<button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
<span>My controls feel laggy or don't stop immediately.</span>
<span class="accordion-arrow transform transition-transform"></span>
</button>
<div class="accordion-content px-4 pb-4">
<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>
<p><strong>Fix:</strong> Add <code>controls.update()</code> to your <code>animate</code> function.</p>
</div>
</div>
<div class="accordion-item">
<button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
<span>The camera zooms in/out but doesn't rotate.</span>
<span class="accordion-arrow transform transition-transform"></span>
</button>
<div class="accordion-content px-4 pb-4">
<p><strong>Cause:</strong> The OrbitControls script has not loaded correctly.</p>
<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>
</div>
</div>
<div class="accordion-item">
<button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
<span>My camera can go below the "ground" plane.</span>
<span class="accordion-arrow transform transition-transform"></span>
</button>
<div class="accordion-content px-4 pb-4">
<p><strong>Cause:</strong> The default polar angle allows for 360-degree vertical rotation.</p>
<p><strong>Fix:</strong> Limit the vertical rotation by setting <code>controls.maxPolarAngle = Math.PI / 2;</code></p>
</div>
</div>
</div>
</div>
</section>
<!-- 3. Importing Models -->
<section id="models" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Importing 3D Models (.glb/.gltf)</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div>
<div class="interactive-canvas-container" id="models-canvas"></div>
<div class="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4 p-4 bg-white border rounded-md">
<div>
<label for="model-pos-x" class="block text-sm font-medium">Position X</label>
<input type="range" id="model-pos-x" min="-5" max="5" step="0.1" value="0" class="w-full">
</div>
<div>
<label for="model-rot-y" class="block text-sm font-medium">Rotation Y</label>
<input type="range" id="model-rot-y" min="-3.14" max="3.14" step="0.1" value="0" class="w-full">
</div>
<div>
<label for="model-scale" class="block text-sm font-medium">Scale</label>
<input type="range" id="model-scale" min="0.1" max="3" step="0.1" value="1.5" class="w-full">
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2 justify-center">
<label class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition cursor-pointer">
Upload Model (.glb/.gltf)
<input type="file" id="model-upload" class="hidden" accept=".glb, .gltf">
</label>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Model Loader & Transformation Code</h3>
<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>
<pre><code class="language-javascript">{`
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'path/to/model.glb',
function (gltf) {
const model = gltf.scene;
// Transformations applied here
model.position.set(0, -1, 0);
model.scale.set(1.5, 1.5, 1.5);
scene.add(model);
},
undefined,
function (error) {
console.error('An error occurred while loading the model:', error);
}
);
`}</code></pre>
</div>
</div>
</section>
<!-- 4. Physics with Cannon.js -->
<section id="physics" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">The Physics Engine: Cannon.js</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div>
<div class="interactive-canvas-container" id="physics-canvas"></div>
<div class="mt-4 flex flex-wrap gap-2 justify-center">
<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>
<label class="flex items-center space-x-2 px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm">
<input type="checkbox" id="physics-debug" class="form-checkbox h-4 w-4 text-gray-600 transition-colors">
<span class="text-sm font-medium text-gray-700">Debug Physics</span>
</label>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Connecting Physics to Rendering</h3>
<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>
<pre><code class="language-javascript">{`
import * as CANNON from 'cannon-es';
import * as CannonDebugRenderer from 'cannon-es-debugger';
const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
const cannonDebugRenderer = new CannonDebugRenderer.default(scene, world);
// Create the physical ground plane
const groundBody = new CANNON.Body({ mass: 0 });
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
// Update both in the animation loop
function animate() {
requestAnimationFrame(animate);
world.fixedStep();
cannonDebugRenderer.update();
// Sync each Three.js mesh with its Cannon.js body
renderer.render(scene, camera);
}
animate();
`}</code></pre>
</div>
</div>
</section>
<!-- 5. Particle & Fluid Simulation -->
<section id="particles" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">Buoyancy and Density Simulation</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div>
<div class="interactive-canvas-container" id="particles-canvas"></div>
<div class="mt-4 flex flex-wrap gap-2 justify-center">
<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>
<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>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">Simulation Logic</h3>
<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>
<pre><code class="language-javascript">{`
// The main animation loop
function animate() {
requestAnimationFrame(animate);
// Update particle positions and apply physics
for (let i = 0; i < particleCount * 3; i += 3) {
const particleIndex = i / 3;
const currentY = positions[i + 1];
// If the particle is below the water surface
if (currentY < water.position.y) {
if (particleTypes[particleIndex] === 'wood') {
// Wood floats: Stop it from sinking further
positions[i + 1] = water.position.y;
velocities[i + 1] = 0;
} else if (particleTypes[particleIndex] === 'metal') {
// Metal sinks: Continue downward motion with some resistance
velocities[i + 1] *= 0.98;
}
} else {
// In freefall, apply simple gravity
velocities[i + 1] -= 0.005;
}
positions[i] += velocities[i];
positions[i + 1] += velocities[i + 1];
positions[i + 2] += velocities[i + 2];
}
particleGeometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
`}</code></pre>
</div>
</div>
</section>
<!-- 6. Debugging -->
<section id="debugging" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">A Game Developer's Debugging Toolkit</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div class="space-y-4">
<h3 class="text-xl font-semibold mb-2">Interactive Debug Console</h3>
<div class="debug-console" id="debug-console">
<div class="debug-line debug-info" data-fix="fix1">▷ INFO: THREE.WebGLRenderer 164</div>
<div class="debug-line debug-warn" data-fix="fix2">▷ WARN: Scene has no lights. Objects may appear black.</div>
<div class="debug-line debug-error" data-fix="fix3">▷ ERROR: TypeError: Cannot read properties of undefined (reading 'scene')</div>
<div class="debug-line debug-error" data-fix="fix4">▷ ERROR: Failed to load resource: net::ERR_FILE_NOT_FOUND Horse.glb</div>
<div class="debug-line debug-info" data-fix="fix5">▷ INFO: Animation loop started.</div>
<div class="debug-line debug-error" data-fix="fix6">▷ ERROR: Uncaught TypeError: Cannot set properties of null (setting 'position')</div>
</div>
</div>
<div class="p-4 bg-white border rounded-md" id="debug-fix-display">
<h3 class="text-xl font-semibold mb-2">Analysis & Fix</h3>
<p class="text-gray-500">Click on an error in the console to see a detailed explanation and solution here.</p>
</div>
</div>
</section>
<!-- 7. Model Viewer -->
<section id="model-viewer" class="space-y-6 hidden">
<h2 class="text-3xl font-bold" style="color: #a58d6f;">The Easy Way: Google's &lt;model-viewer&gt;</h2>
<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>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<div>
<h3 class="text-xl font-semibold mb-2">Live Example</h3>
<div id="model-viewer-container">
<model-viewer
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
alt="A 3D model of an astronaut"
ar
auto-rotate
camera-controls
style="width: 100%; height: 400px; background-color: #f0f0f0; border-radius: 8px;"
></model-viewer>
</div>
</div>
<div>
<h3 class="text-xl font-semibold mb-2">When to Use Which?</h3>
<div class="space-y-4">
<div>
<h4 class="font-semibold text-lg" style="color: #846c5b;">Use <code>&lt;model-viewer&gt;</code> when:</h4>
<ul class="list-disc list-inside text-gray-700">
<li>You need to display a single, pre-made 3D model.</li>
<li>The main goal is showcasing the model (e.g., e-commerce, portfolio).</li>
<li>You want easy integration with Augmented Reality (AR).</li>
<li>You prefer a declarative HTML approach over writing JavaScript.</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-lg" style="color: #846c5b;">Use <code>Three.js</code> when:</h4>
<ul class="list-disc list-inside text-gray-700">
<li>You are building a game or a highly interactive simulation.</li>
<li>You need to manage multiple objects, physics, and complex logic.</li>
<li>You require custom shaders, post-processing effects, or particle systems.</li>
<li>You need full programmatic control over every aspect of the 3D scene.</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</main>
</div>
<script type="module">
import * as THREE from 'https://cdn.skypack.dev/three@0.132.2';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/loaders/GLTFLoader.js';
import * as CANNON from 'https://cdn.skypack.dev/cannon-es@0.20.0';
import CannonDebugger from 'https://cdn.skypack.dev/cannon-es-debugger@1.0.0';
import { Water } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/objects/Water.js';
const tabs = document.querySelectorAll('#tab-nav button');
const sections = document.querySelectorAll('#tab-content section');
const disposers = {};
// Function to clean up a scene
const cleanupScene = (canvasId) => {
const canvasContainer = document.getElementById(canvasId);
if (!canvasContainer) return;
const canvas = canvasContainer.querySelector('canvas');
if (canvas) {
if (disposers[canvasId]) {
disposers[canvasId]();
delete disposers[canvasId];
}
canvas.parentNode.removeChild(canvas);
}
};
// Function to set up the fundamentals scene
const setupFundamentals = () => {
const container = document.getElementById('fundamentals-canvas');
if (!container) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xe7e5e4);
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 3;
const handleResize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
let animationFrameId;
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
cube.rotation.x += 0.005;
cube.rotation.y += 0.005;
renderer.render(scene, camera);
};
animate();
window.addEventListener('resize', handleResize);
document.getElementById('fundamentals-move-x').onclick = () => {
cube.position.x += 0.5;
};
document.getElementById('fundamentals-move-y').onclick = () => {
cube.position.y += 0.5;
};
document.getElementById('fundamentals-color').onclick = () => {
cube.material.color.setHex(Math.random() * 0xffffff);
};
disposers['fundamentals-canvas'] = () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
scene.children.forEach(child => scene.remove(child));
renderer.dispose();
};
};
// Function to set up the controls scene
const setupControls = () => {
const container = document.getElementById('controls-canvas');
if (!container) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xe7e5e4);
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // an animation loop is required when damping is enabled
controls.dampingFactor = 0.05;
const handleResize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
let animationFrameId;
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
window.addEventListener('resize', handleResize);
disposers['controls-canvas'] = () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
scene.children.forEach(child => scene.remove(child));
renderer.dispose();
};
};
// Function to set up the models scene
const setupModels = () => {
const container = document.getElementById('models-canvas');
if (!container) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xe7e5e4);
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 1.5, 4);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0.5, 0);
const ambientLight = new THREE.AmbientLight(0xffffff, 2);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
dirLight.position.set(5, 10, 7.5);
scene.add(dirLight);
const loader = new GLTFLoader();
let model;
loader.load('https://cdn.jsdelivr.net/npm/three@0.164.1/examples/models/gltf/Horse.glb', (gltf) => {
model = gltf.scene;
model.position.y = -1;
model.scale.set(1.5, 1.5, 1.5);
scene.add(model);
});
const handleResize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
let animationFrameId;
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
window.addEventListener('resize', handleResize);
document.getElementById('model-pos-x').oninput = (e) => {
if (model) model.position.x = parseFloat(e.target.value);
};
document.getElementById('model-rot-y').oninput = (e) => {
if (model) model.rotation.y = parseFloat(e.target.value);
};
document.getElementById('model-scale').oninput = (e) => {
if (model) model.scale.set(parseFloat(e.target.value), parseFloat(e.target.value), parseFloat(e.target.value));
};
document.getElementById('model-upload').onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
loader.load(url, (gltf) => {
if (model) scene.remove(model);
model = gltf.scene;
model.position.y = -1;
scene.add(model);
});
};
disposers['models-canvas'] = () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
scene.children.forEach(child => scene.remove(child));
renderer.dispose();
};
};
// Function to set up the physics scene
const setupPhysics = () => {
const container = document.getElementById('physics-canvas');
if (!container) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xe7e5e4);
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 7.5);
scene.add(directionalLight);
const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);
const groundMesh = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardMaterial({ color: 0xcccccc }));
groundMesh.rotation.x = -Math.PI / 2;
scene.add(groundMesh);
const meshes = [];
const bodies = [];
let debuggerInstance;
const handleResize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
let animationFrameId;
const timeStep = 1 / 60;
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
world.step(timeStep);
controls.update();
if (document.getElementById('physics-debug').checked) {
if (!debuggerInstance) {
debuggerInstance = new CannonDebugger(scene, world);
}
debuggerInstance.update();
} else {
if (debuggerInstance) {
debuggerInstance.destroy();
debuggerInstance = null;
}
}
for (let i = 0; i < meshes.length; i++) {
meshes[i].position.copy(bodies[i].position);
meshes[i].quaternion.copy(bodies[i].quaternion);
}
renderer.render(scene, camera);
};
animate();
window.addEventListener('resize', handleResize);
document.getElementById('drop-ball').onclick = () => {
const radius = 0.5;
const sphereGeometry = new THREE.SphereGeometry(radius, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0x846c5b });
const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphereMesh.position.y = 10;
scene.add(sphereMesh);
meshes.push(sphereMesh);
const sphereBody = new CANNON.Body({ mass: 1, shape: new CANNON.Sphere(radius) });
sphereBody.position.y = 10;
world.addBody(sphereBody);
bodies.push(sphereBody);
sphereBody.addEventListener('collide', (event) => {
sphereMesh.material.color.setHex(0xff0000);
setTimeout(() => {
sphereMesh.material.color.setHex(0x846c5b);
}, 200);
});
};
disposers['physics-canvas'] = () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
if (debuggerInstance) { debuggerInstance.destroy(); }
scene.children.forEach(child => scene.remove(child));
renderer.dispose();
meshes.length = 0;
bodies.length = 0;
};
};
// Function to set up the particle simulation
const setupParticles = () => {
const container = document.getElementById('particles-canvas');
if (!container) return;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 10, 20);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 0);
const waterGeometry = new THREE.PlaneGeometry(2000, 2000);
const water = new Water(
waterGeometry,
{
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/waternormals.jpg', function (texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
}),
sunDirection: new THREE.Vector3(0, 1, 0),
sunColor: 0x48494b,
waterColor: 0x005577,
distortionScale: 3.7,
fog: scene.fog !== undefined
}
);
water.rotation.x = -Math.PI / 2;
scene.add(water);
const particleCount = 500;
const positions = new Float32Array(particleCount * 3);
const velocities = new Float32Array(particleCount * 3);
const particleTypes = new Array(particleCount).fill(null);
const colors = new Float32Array(particleCount * 3);
const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 0.5,
vertexColors: true,
sizeAttenuation: true,
transparent: true,
opacity: 0.9,
blending: THREE.AdditiveBlending
});
const particleSystem = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particleSystem);
const woodColor = new THREE.Color(0x8B4513);
const metalColor = new THREE.Color(0x708090);
let particleCursor = 0;
const spawnParticles = (type) => {
const count = 50;
const spread = 20;
const startY = 15;
for (let i = 0; i < count; i++) {
const idx = particleCursor * 3;
positions[idx] = (Math.random() - 0.5) * spread;
positions[idx + 1] = startY + Math.random() * 5;
positions[idx + 2] = (Math.random() - 0.5) * spread;
velocities[idx] = 0;
velocities[idx + 1] = -0.3 - Math.random() * 0.3;
velocities[idx + 2] = 0;
particleTypes[particleCursor] = type;
if (type === 'wood') {
woodColor.toArray(colors, idx);
} else {
metalColor.toArray(colors, idx);
}
particleCursor = (particleCursor + 1) % particleCount;
}
particleGeometry.attributes.position.needsUpdate = true;
particleGeometry.attributes.color.needsUpdate = true;
};
const handleResize = () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
};
let animationFrameId;
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
controls.update();
water.material.uniforms['time'].value += 1.0 / 60.0;
for (let i = 0; i < particleCount * 3; i += 3) {
const particleIndex = i / 3;
const currentY = positions[i + 1];
if (currentY < water.position.y) {
if (particleTypes[particleIndex] === 'wood') {
positions[i + 1] = water.position.y;
velocities[i + 1] = 0;
} else if (particleTypes[particleIndex] === 'metal') {
velocities[i + 1] *= 0.98;
}
} else {
velocities[i + 1] -= 0.005;
}
positions[i] += velocities[i];
positions[i + 1] += velocities[i + 1];
positions[i + 2] += velocities[i + 2];
}
particleGeometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
};
animate();
window.addEventListener('resize', handleResize);
document.getElementById('drop-wood').onclick = () => spawnParticles('wood');
document.getElementById('drop-metal').onclick = () => spawnParticles('metal');
disposers['particles-canvas'] = () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
scene.children.forEach(child => scene.remove(child));
renderer.dispose();
};
};
const setupDebugging = () => {
const fixes = {
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.",
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.",
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.",
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.",
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.",
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) { ... }.",
};
const display = document.getElementById('debug-fix-display');
const displayTitle = display.querySelector('h3');
const displayContent = display.querySelector('p');
document.querySelectorAll('.debug-line').forEach(line => {
line.onclick = (e) => {
const fixKey = e.currentTarget.getAttribute('data-fix');
const titleText = e.currentTarget.textContent.replace('▷ ', '');
displayTitle.textContent = "Analysis & Fix";
displayContent.innerHTML = `<strong>${titleText}</strong><br>${fixes[fixKey]}`;
};
});
disposers['debugging'] = () => {
document.querySelectorAll('.debug-line').forEach(line => line.onclick = null);
displayContent.innerHTML = "Click on an error in the console to see a detailed explanation and solution here.";
};
};
// Function to set up the model viewer tab
const setupModelViewer = () => {
const script = document.createElement('script');
script.type = 'module';
script.src = 'https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js';
document.head.appendChild(script);
disposers['model-viewer'] = () => {
// No cleanup for this simple setup, as the component manages itself
};
};
const tabSetupFunctions = {
'fundamentals': setupFundamentals,
'controls': setupControls,
'models': setupModels,
'physics': setupPhysics,
'particles': setupParticles,
'debugging': setupDebugging,
'model-viewer': setupModelViewer,
};
const switchTab = (tabId) => {
// Cleanup the current scene if one exists
sections.forEach(section => {
if (!section.classList.contains('hidden')) {
const currentTabId = section.id;
if (disposers[currentTabId]) {
disposers[currentTabId]();
}
}
});
// Hide all sections and deactivate all tabs
sections.forEach(section => section.classList.add('hidden'));
tabs.forEach(tab => tab.classList.remove('tab-active'));
tabs.forEach(tab => tab.classList.add('tab-inactive'));
// Show the selected section and activate its tab
document.getElementById(tabId).classList.remove('hidden');
document.getElementById(tabId + '-tab').classList.remove('tab-inactive');
document.getElementById(tabId + '-tab').classList.add('tab-active');
// Run the setup function for the new tab
if (tabSetupFunctions[tabId]) {
tabSetupFunctions[tabId]();
}
};
// Add event listeners for tab clicks
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
const tabId = e.target.id.replace('-tab', '');
switchTab(tabId);
});
});
document.querySelectorAll('.accordion-button').forEach(button => {
button.addEventListener('click', (e) => {
const content = e.currentTarget.nextElementSibling;
e.currentTarget.classList.toggle('open');
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = content.scrollHeight + "px";
}
});
});
// Initial setup for the first tab
window.onload = () => {
switchTab('fundamentals');
};
</script>
</body>
</html>