In this tutorial, we'll build a more complex 3D scene with multiple objects, different geometries, lighting, and interactivity. You'll learn how to create engaging 3D environments using Ninth.js.
We'll create an interactive 3D scene featuring:
- Multiple geometric shapes (cubes, spheres, planes)
- Different material types
- Dynamic lighting
- Camera controls
- Simple animations
- Interactive mouse controls
- Basic understanding of Ninth.js (see Getting Started)
- HTML and JavaScript knowledge
- A web server for testing
Let's start with a complete example and then break it down:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ninth.js - Interactive 3D Scene</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
font-family: Arial, sans-serif;
}
#controls {
position: absolute;
top: 10px;
right: 10px;
color: white;
background-color: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
z-index: 100;
}
#controls h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
#controls label {
display: block;
margin: 5px 0;
font-size: 14px;
}
#controls input[type="range"] {
width: 200px;
}
#fps-counter {
position: absolute;
top: 10px;
left: 10px;
color: white;
background-color: rgba(0, 0, 0, 0.7);
padding: 8px;
border-radius: 5px;
font-family: monospace;
}
</style>
</head>
<body>
<div id="controls">
<h3>Scene Controls</h3>
<label>
Rotate Speed: <input type="range" id="speed" min="0" max="3" step="0.1" value="1">
</label>
<label>
Light Intensity: <input type="range" id="light" min="0" max="2" step="0.1" value="1">
</label>
<button id="addObject">Add Random Object</button>
<button id="reset">Reset Scene</button>
</div>
<div id="fps-counter">
FPS: <span id="fps">0</span>
</div>
<canvas id="canvas"></canvas>
<script src="dist/9th.umd.js"></script>
<script>
class SceneController {
constructor() {
this.canvas = document.getElementById('canvas');
this.scene = null;
this.camera = null;
this.renderer = null;
this.objects = [];
this.animationSpeed = 1;
this.lightIntensity = 1;
this.frameCount = 0;
this.lastTime = performance.now();
this.init();
this.setupEventListeners();
this.animate();
}
init() {
// Setup canvas
this.setupCanvas();
// Create engine
this.engine = new NinthJS.Engine(this.canvas, {
antialias: true,
alpha: false,
powerPreference: 'high-performance'
});
// Create scene
this.scene = new NinthJS.Scene();
this.scene.setBackground('linear-gradient(to bottom, #001122, #003366)');
// Create camera
this.camera = new NinthJS.PerspectiveCamera(
75,
this.canvas.width / this.canvas.height,
0.1,
1000
);
this.camera.setPosition(0, 5, 15);
this.camera.lookAt(0, 0, 0);
// Create renderer
this.renderer = new NinthJS.Renderer(this.canvas);
// Setup lighting
this.setupLights();
// Create initial objects
this.createInitialScene();
// Handle window resize
window.addEventListener('resize', () => this.handleResize());
}
setupCanvas() {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
}
setupLights() {
// Ambient light for overall illumination
const ambientLight = new NinthJS.AmbientLight(0.3 * this.lightIntensity, '#4040ff');
this.scene.add(ambientLight);
// Directional light for main lighting
const directionalLight = new NinthJS.DirectionalLight(0.8 * this.lightIntensity, '#ffffff');
directionalLight.setDirection(-1, -2, -1);
this.scene.add(directionalLight);
// Point lights for accent lighting
const pointLight1 = new NinthJS.PointLight(0.5 * this.lightIntensity, '#ff4444');
pointLight1.setPosition(5, 5, 5);
this.scene.add(pointLight1);
const pointLight2 = new NinthJS.PointLight(0.5 * this.lightIntensity, '#44ff44');
pointLight2.setPosition(-5, -5, 5);
this.scene.add(pointLight2);
}
createInitialScene() {
// Create a ground plane
this.createGround();
// Create central object
this.createCentralObject();
// Create surrounding objects
this.createSurroundingObjects();
}
createGround() {
const groundGeometry = new NinthJS.PlaneGeometry(20, 20);
const groundMaterial = new NinthJS.BasicMaterial({
color: '#333333',
wireframe: false
});
const ground = new NinthJS.Mesh(groundGeometry);
ground.material = groundMaterial;
ground.setRotation(-Math.PI / 2, 0, 0);
ground.setPosition(0, -2, 0);
this.scene.add(ground);
}
createCentralObject() {
const geometry = new NinthJS.SphereGeometry(2, 32, 16);
const material = new NinthJS.PhongMaterial({
color: '#4488ff',
shininess: 100,
specular: '#ffffff'
});
const sphere = new NinthJS.Mesh(geometry);
sphere.material = material;
sphere.setPosition(0, 0, 0);
this.scene.add(sphere);
this.objects.push({ mesh: sphere, type: 'sphere' });
}
createSurroundingObjects() {
const objectTypes = [
() => new NinthJS.BoxGeometry(1, 1, 1),
() => new NinthJS.SphereGeometry(0.8, 16, 8),
() => new NinthJS.PlaneGeometry(1.5, 1.5)
];
const materials = [
new NinthJS.BasicMaterial({ color: '#ff6666' }),
new NinthJS.PhongMaterial({ color: '#66ff66', shininess: 50 }),
new NinthJS.BasicMaterial({ color: '#6666ff' })
];
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const distance = 6 + Math.random() * 2;
const x = Math.cos(angle) * distance;
const z = Math.sin(angle) * distance;
const y = (Math.random() - 0.5) * 4;
const geometryIndex = Math.floor(Math.random() * objectTypes.length);
const geometry = objectTypes[geometryIndex]();
const material = materials[geometryIndex % materials.length];
const mesh = new NinthJS.Mesh(geometry);
mesh.material = material;
mesh.setPosition(x, y, z);
// Random rotation
mesh.setRotation(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
this.scene.add(mesh);
this.objects.push({
mesh: mesh,
type: geometryIndex === 0 ? 'box' : geometryIndex === 1 ? 'sphere' : 'plane',
orbitAngle: angle,
orbitDistance: distance,
orbitSpeed: 0.5 + Math.random() * 0.5
});
}
}
addRandomObject() {
const objectTypes = [
{ geometry: () => new NinthJS.BoxGeometry(0.8 + Math.random() * 0.8, 0.8 + Math.random() * 0.8, 0.8 + Math.random() * 0.8), type: 'box' },
{ geometry: () => new NinthJS.SphereGeometry(0.5 + Math.random() * 0.5, 16, 8), type: 'sphere' },
{ geometry: () => new NinthJS.PlaneGeometry(1 + Math.random(), 1 + Math.random()), type: 'plane' }
];
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff'];
const objectConfig = objectTypes[Math.floor(Math.random() * objectTypes.length)];
const geometry = objectConfig.geometry();
const material = new NinthJS.BasicMaterial({
color: colors[Math.floor(Math.random() * colors.length)]
});
const mesh = new NinthJS.Mesh(geometry);
mesh.material = material;
// Random position
mesh.setPosition(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 6,
(Math.random() - 0.5) * 10
);
// Random rotation
mesh.setRotation(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
this.scene.add(mesh);
this.objects.push({
mesh: mesh,
type: objectConfig.type,
wobbleSpeed: 0.5 + Math.random() * 1,
wobbleAmount: 0.5 + Math.random() * 0.5
});
}
setupEventListeners() {
// Control sliders
document.getElementById('speed').addEventListener('input', (e) => {
this.animationSpeed = parseFloat(e.target.value);
});
document.getElementById('light').addEventListener('input', (e) => {
this.lightIntensity = parseFloat(e.target.value);
this.updateLighting();
});
// Buttons
document.getElementById('addObject').addEventListener('click', () => {
this.addRandomObject();
});
document.getElementById('reset').addEventListener('click', () => {
this.resetScene();
});
// Mouse interaction
this.canvas.addEventListener('mousemove', (e) => {
this.handleMouseMove(e);
});
}
updateLighting() {
const lights = this.scene.lights;
lights.forEach(light => {
if (light instanceof NinthJS.AmbientLight) {
light.intensity = 0.3 * this.lightIntensity;
} else if (light instanceof NinthJS.DirectionalLight ||
light instanceof NinthJS.PointLight) {
light.intensity = 0.8 * this.lightIntensity;
}
});
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
// Convert to normalized device coordinates
const ndcX = x * 2 - 1;
const ndcY = -(y * 2 - 1);
// Subtle camera movement
this.camera.setPosition(ndcX * 2, 5 + ndcY * 2, 15);
this.camera.lookAt(0, 0, 0);
}
handleResize() {
this.setupCanvas();
this.camera.aspect = this.canvas.width / this.canvas.height;
this.camera.updateProjectionMatrix();
}
animate() {
requestAnimationFrame(() => this.animate());
const currentTime = performance.now();
const deltaTime = currentTime - this.lastTime;
// Update FPS counter
this.frameCount++;
if (currentTime - this.lastTime >= 1000) {
document.getElementById('fps').textContent = Math.round((this.frameCount * 1000) / (currentTime - this.lastTime));
this.frameCount = 0;
this.lastTime = currentTime;
}
// Update objects
this.updateObjects(currentTime * 0.001 * this.animationSpeed);
// Render
this.renderer.render(this.scene, this.camera);
}
updateObjects(time) {
this.objects.forEach((obj, index) => {
const mesh = obj.mesh;
if (obj.type === 'sphere' && index === 0) {
// Central sphere - complex animation
mesh.setRotation(time * 0.5, time * 0.8, 0);
} else if (obj.orbitAngle !== undefined) {
// Orbiting objects
obj.orbitAngle += obj.orbitSpeed * 0.01 * this.animationSpeed;
const x = Math.cos(obj.orbitAngle) * obj.orbitDistance;
const z = Math.sin(obj.orbitAngle) * obj.orbitDistance;
mesh.setPosition(x, mesh.getPosition().y, z);
mesh.setRotation(
time * obj.orbitSpeed * 0.5,
time * obj.orbitSpeed * 0.7,
time * obj.orbitSpeed * 0.3
);
} else if (obj.wobbleSpeed !== undefined) {
// Random objects - wobble animation
const wobble = Math.sin(time * obj.wobbleSpeed) * obj.wobbleAmount;
mesh.setRotation(wobble, time * 0.5, wobble * 0.5);
// Gentle floating
const basePos = mesh.getPosition();
mesh.setPosition(basePos.x, basePos.y + Math.sin(time + index) * 0.1, basePos.z);
}
});
}
resetScene() {
// Remove all objects except ground
this.objects.forEach(obj => {
if (obj.type !== 'ground') {
this.scene.remove(obj.mesh);
}
});
this.objects = this.objects.filter(obj => obj.type === 'ground');
// Recreate initial objects
this.createCentralObject();
this.createSurroundingObjects();
}
}
// Initialize the scene
try {
const sceneController = new SceneController();
console.log('🎉 Interactive 3D scene loaded successfully!');
} catch (error) {
console.error('Error initializing scene:', error);
}
</script>
</body>
</html>We organized our code into a SceneController class to manage:
- Scene state and objects
- User interface controls
- Animation and updates
- Event handling
Ground Plane:
const groundGeometry = new NinthJS.PlaneGeometry(20, 20);
const groundMaterial = new NinthJS.BasicMaterial({ color: '#333333' });
const ground = new NinthJS.Mesh(groundGeometry);
ground.setRotation(-Math.PI / 2, 0, 0); // Rotate to be horizontal
ground.setPosition(0, -2, 0);Geometric Primitives:
BoxGeometry(width, height, depth)- For cubes and rectangular objectsSphereGeometry(radius, widthSegments, heightSegments)- For spherical objectsPlaneGeometry(width, height)- For flat surfaces
Ambient Light:
const ambientLight = new NinthJS.AmbientLight(0.3, '#4040ff');
scene.add(ambientLight);Provides overall illumination with a blue tint.
Directional Light:
const directionalLight = new NinthJS.DirectionalLight(0.8, '#ffffff');
directionalLight.setDirection(-1, -2, -1);
scene.add(directionalLight);Simulates sunlight coming from a specific direction.
Point Lights:
const pointLight = new NinthJS.PointLight(0.5, '#ff4444');
pointLight.setPosition(5, 5, 5);
scene.add(pointLight);Creates localized lighting effects from a specific point.
BasicMaterial:
const basicMaterial = new NinthJS.BasicMaterial({ color: '#4488ff' });Simple unlit material with solid colors.
PhongMaterial:
const phongMaterial = new NinthJS.PhongMaterial({
color: '#4488ff',
shininess: 100,
specular: '#ffffff'
});Provides realistic lighting with specular highlights.
Rotation Animation:
mesh.setRotation(time * 0.5, time * 0.8, 0);Orbital Movement:
obj.orbitAngle += obj.orbitSpeed * 0.01;
const x = Math.cos(obj.orbitAngle) * obj.orbitDistance;
const z = Math.sin(obj.orbitAngle) * obj.orbitDistance;
mesh.setPosition(x, mesh.getPosition().y, z);Wobble Effect:
const wobble = Math.sin(time * obj.wobbleSpeed) * obj.wobbleAmount;
mesh.setRotation(wobble, time * 0.5, wobble * 0.5);handleMouseMove(e) {
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
this.camera.setPosition(x * 2, 5 + y * 2, 15);
this.camera.lookAt(0, 0, 0);
}- Speed slider: Controls animation speed
- Light intensity: Adjusts all light intensities
- Add objects: Randomly generates new 3D objects
- Reset: Restores the initial scene
Instead of creating and destroying objects frequently, maintain an object pool:
// Reuse objects instead of creating new ones
function getObjectFromPool() {
return this.objectPool.find(obj => !obj.active) || createNewObject();
}Use simpler geometries for distant objects:
function createLODObject(distance) {
let segments = 32; // High detail
if (distance > 10) segments = 8; // Low detail
return new NinthJS.SphereGeometry(radius, segments, Math.floor(segments / 2));
}Only render objects that are visible:
function isObjectVisible(mesh, camera) {
// Simple frustum culling
const objectPosition = mesh.getPosition();
const cameraPosition = camera.getPosition();
const distance = objectPosition.distanceTo(cameraPosition);
return distance < 50; // Only render objects within 50 units
}const groundObjects = new NinthJS.Group();
const skyObjects = new NinthJS.Group();
const interactiveObjects = new NinthJS.Group();
scene.add(groundObjects);
scene.add(skyObjects);
scene.add(interactiveObjects);Organize objects into layers for efficient rendering:
const backgroundLayer = new NinthJS.Layer();
const mainLayer = new NinthJS.Layer();
const uiLayer = new NinthJS.Layer();class AssetManager {
constructor() {
this.materials = new Map();
this.geometries = new Map();
}
getMaterial(name) {
if (!this.materials.has(name)) {
this.materials.set(name, this.createMaterial(name));
}
return this.materials.get(name);
}
getGeometry(name) {
if (!this.geometries.has(name)) {
this.geometries.set(name, this.createGeometry(name));
}
return this.geometries.get(name);
}
}class ObjectFactory {
static createBox(x, y, z, color) {
const geometry = new NinthJS.BoxGeometry(1, 1, 1);
const material = new NinthJS.BasicMaterial({ color });
const mesh = new NinthJS.Mesh(geometry);
mesh.material = material;
mesh.setPosition(x, y, z);
return mesh;
}
static createSphere(x, y, z, radius, color) {
const geometry = new NinthJS.SphereGeometry(radius, 16, 8);
const material = new NinthJS.PhongMaterial({ color });
const mesh = new NinthJS.Mesh(geometry);
mesh.material = material;
mesh.setPosition(x, y, z);
return mesh;
}
}const SceneState = {
LOADING: 'loading',
RUNNING: 'running',
PAUSED: 'paused',
ERROR: 'error'
};
class ManagedScene {
constructor() {
this.state = SceneState.LOADING;
this.init();
}
init() {
try {
// Scene initialization code
this.state = SceneState.RUNNING;
} catch (error) {
this.state = SceneState.ERROR;
console.error('Scene initialization failed:', error);
}
}
}// Add wireframe helpers
const wireframeMaterial = new NinthJS.WireframeMaterial();
const wireframeHelper = new NinthJS.Mesh(geometry, wireframeMaterial);
// Add bounding box helpers
const boundingBoxHelper = new NinthJS.BoundingBoxHelper(mesh);
scene.add(boundingBoxHelper);class PerformanceMonitor {
constructor() {
this.frameTime = 0;
this.memoryUsage = 0;
}
update() {
const start = performance.now();
// ... render code
const end = performance.now();
this.frameTime = end - start;
}
getStats() {
return {
fps: Math.round(1000 / this.frameTime),
frameTime: this.frameTime,
triangles: this.getTriangleCount(),
drawCalls: this.getDrawCallCount()
};
}
}| Issue | Solution |
|---|---|
| Objects not visible | Check camera position and lighting |
| Poor performance | Reduce geometry complexity, optimize materials |
| Strange colors | Ensure proper lighting setup |
| Objects clipping | Adjust near/far planes of camera |
| Memory leaks | Properly dispose of unused geometries and materials |
- Canvas is properly sized
- Camera is positioned correctly
- Lighting is set up appropriately
- Materials are assigned correctly
- Objects are within camera view
- Performance metrics are monitored
Now that you've created a complex 3D scene, you're ready to explore:
- Working with Materials - Dive deeper into material systems and textures
- Animation Basics - Learn advanced animation techniques
- Loading 3D Models - Import external 3D assets
- Physics Integration - Add realistic physics to your scenes
Congratulations! You've created your first interactive 3D scene with Ninth.js! 🎉