Lesson 06-Three.js Application Development Practice

Three.js Project Practice Building a 3D Scene

Basic 3D Scene Setup

Creating a basic 3D scene typically involves setting up the camera, lights, geometry, material, and renderer:

<!DOCTYPE html>
<html>
<head>
    <title>Three.js 3D Scene</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/jsm/controls/OrbitControls.js"></script>
    <script>
        // Initialize scene
        const scene = new THREE.Scene();

        // Create camera
        const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.z = 5;

        // Add lighting
        const ambientLight = new THREE.AmbientLight(0xffffff); // Ambient light
        scene.add(ambientLight);
        const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // Directional light
        directionalLight.position.set(1, 1, 1).normalize();
        scene.add(directionalLight);

        // Create geometry
        const geometry = new THREE.BoxGeometry(1, 1, 1);

        // Create material
        const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); // Lambert material with basic lighting

        // Create mesh
        const cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        // Create renderer
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // Add OrbitControls
        const controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.update();

        // Render loop
        function animate() {
            requestAnimationFrame(animate);

            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;

            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>
  • Create Scene: new THREE.Scene() creates a 3D scene container.
  • Create Camera: new THREE.PerspectiveCamera() sets up a perspective camera with field of view, aspect ratio, near, and far clipping planes.
  • Add Lighting: THREE.AmbientLight and THREE.DirectionalLight provide ambient and directional lighting for visual enhancement.
  • Create Geometry: new THREE.BoxGeometry() defines a cube’s geometry.
  • Create Material: new THREE.MeshLambertMaterial() creates a Lambert material with simple ambient light reflection.
  • Create Mesh: new THREE.Mesh() combines geometry and material, added to the scene.
  • Create Renderer: new THREE.WebGLRenderer() initializes a WebGL renderer, sized and appended to the HTML.
  • Add OrbitControls: Enables user interaction (rotate, pan, zoom) via mouse or touch.
  • Animation Loop: requestAnimationFrame(animate) drives the animation, rotating the cube and rendering each frame with renderer.render().

Adding Extensions

Enhance the 3D scene with textures, skyboxes, ground, interactivity, and more.

Loading Textures

Add textures to the cube for a realistic appearance.

// Load texture
const textureLoader = new THREE.TextureLoader();
const cubeTexture = textureLoader.load('path/to/your/cubetexture.jpg');

// Apply texture to material
material.map = cubeTexture;

Creating a Skybox

A skybox provides a rich background environment.

// Create skybox texture
const cubeTextureLoader = new THREE.CubeTextureLoader();
const skyboxTexture = cubeTextureLoader.setPath('path/to/skybox/').load([
    'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg'
]);

// Create skybox
const skybox = new THREE.Mesh(
    new THREE.BoxGeometry(100, 100, 100),
    new THREE.MeshBasicMaterial({ envMap: skyboxTexture, side: THREE.BackSide })
);
scene.add(skybox);

Adding a Ground

Add a ground plane using PlaneGeometry and a material.

const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2; // Make ground horizontal
ground.position.y = -1; // Adjust ground position
scene.add(ground);

Dynamic Lighting

Use a point light that follows mouse movement for interactivity.

let mouseX = 0;
let mouseY = 0;

window.addEventListener('mousemove', (event) => {
    mouseX = event.clientX;
    mouseY = event.clientY;
});

const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);

function updateLightPosition() {
    pointLight.position.set(mouseX * -0.005, mouseY * -0.005, 1);
}

function animate() {
    requestAnimationFrame(animate);

    // Update light position
    updateLightPosition();

    // ... other animation code
    renderer.render(scene, camera);
}

Using Post-Processing

Post-processing enhances visuals with effects like depth of field or bloom.

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';

const composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
composer.addPass(bloomPass);

function animate() {
    requestAnimationFrame(animate);

    // Update scene

    // Render with post-processing
    composer.render();
}

Add first-person controls for an immersive exploration experience.

import { FirstPersonControls } from 'three/examples/jsm/controls/FirstPersonControls.js';

// Create controls
const controls = new FirstPersonControls(camera, renderer.domElement);
controls.lookSpeed = 0.05; // Rotation speed
controls.movementSpeed = 1; // Movement speed

// Update controls
function updateControls() {
    controls.update(0.05); // deltaTime, simplified
}

function animate() {
    requestAnimationFrame(animate);

    // Update controls
    updateControls();

    // ... other animation code
    renderer.render(scene, camera);
}

Animation and Skeletal Animation

Three.js supports skeletal animations for characters or complex motion.

// Load animated model
const loader = new THREE.GLTFLoader();
loader.load('path/to/model.gltf', (gltf) => {
    const model = gltf.scene;
    scene.add(model);

    // Set up animation mixer
    const mixer = new THREE.AnimationMixer(model);
    const clip = gltf.animations[0]; // Assume first animation
    const action = mixer.clipAction(clip);
    action.play();

    // Update animations
    function updateAnimations() {
        mixer.update(0.05); // Update mixer with deltaTime
    }

    function animate() {
        requestAnimationFrame(animate);

        // Update animations
        updateAnimations();

        // ... other animation code
        renderer.render(scene, camera);
    }
});

Particle System

Particle systems simulate natural phenomena like rain, snow, fire, or smoke.

const particleCount = 1000;
const particles = new THREE.BufferGeometry();
const particleMaterial = new THREE.PointsMaterial({
    color: 0xffffff,
    size: 0.1
});

const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
    positions[i * 3] = Math.random() * 200 - 100;
    positions[i * 3 + 1] = Math.random() * 200 - 100;
    positions[i * 3 + 2] = Math.random() * 200 - 100;
}
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const particleSystem = new THREE.Points(particles, particleMaterial);
scene.add(particleSystem);

Integrating a Physics Engine

For realistic interactions like collisions and gravity, integrate a physics engine like Cannon.js or Ammo.js.

Using Cannon.js:

import * as CANNON from 'cannon-es';

// Create world
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // Set gravity

// Create ground
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0 });
groundBody.addShape(groundShape);
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); // Rotate ground
world.addBody(groundBody);

// Create cube physics object
const boxShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
const boxBody = new CANNON.Body({ mass: 1 });
boxBody.addShape(boxShape);
boxBody.position.set(0, 5, 0);
world.addBody(boxBody);

// Update physics world
function updatePhysics() {
    world.step(1 / 60); // Update at 60 FPS
    cube.position.copy(boxBody.position);
    cube.quaternion.copy(boxBody.quaternion);
}

// Call updatePhysics in animation loop
function animate() {
    requestAnimationFrame(animate);

    // Update physics world
    updatePhysics();

    // ... other animation code
    renderer.render(scene, camera);
}

Shadows

Add shadows to enhance realism.

// Enable shadow casting for light
directionalLight.castShadow = true;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;

// Scene receives shadows
scene.receiveShadow = true;

// Object casts shadows
cube.castShadow = true;

// Set renderer shadow properties
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Using DRACOLoader for Draco-Compressed Models

Draco compression reduces large 3D model file sizes.

import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('path/to/draco/decoder/'); // Draco decoder path
const loader = new GLTFLoader();
loader.setDRACOLoader(dracoLoader);

loader.load('path/to/model.gltf', (gltf) => {
    scene.add(gltf.scene);
}, undefined, (error) => {
    console.error(error);
});

Implementing Responsive Layout

Ensure the 3D scene adapts to different devices and screen sizes.

window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

Three.js Project Practice Terrain and Environment Building

Building complex 3D scenes, such as cityscapes or interior designs, involves multiple steps including terrain generation, building models, texture mapping, lighting, shadows, and interactivity. Complex projects require advanced terrain generation algorithms and extensive model data.

Terrain Generation

Use heightmap-based terrain generation with THREE.HeightmapGeometry or the THREE.Terrain plugin (requires separate installation if used).

import * as THREE from 'three';
import { Terrain } from 'three-terrain';

// Load heightmap
const heightmapLoader = new THREE.ImageLoader();
heightmapLoader.load('path/to/heightmap.png', (image) => {
    // Generate terrain
    const terrain = Terrain({
        heightmap: image,
        scale: 50, // Terrain scale
        exaggeration: 1.5, // Height exaggeration
        widthSegments: 100, // Width segments
        heightSegments: 100, // Height segments
    });

    const geometry = new THREE.HeightmapGeometry(terrain.data, terrain.scale, terrain.widthSegments, terrain.heightSegments, 0, 1);
    const material = new THREE.MeshStandardMaterial({ color: 0x8BC34A, wireframe: false });
    const terrainMesh = new THREE.Mesh(geometry, material);
    scene.add(terrainMesh);
});

Building Models

Load 3D models (e.g., glTF or FBX format) to represent buildings.

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
loader.load('path/to/building.gltf', (gltf) => {
    const building = gltf.scene;
    scene.add(building);
}, undefined, (error) => {
    console.error(error);
});

Environment and Lighting

Set up ambient lighting and shadows.

// Ambient light
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);

// Main light
const mainLight = new THREE.DirectionalLight(0xffffff, 1);
mainLight.castShadow = true;
mainLight.shadow.camera.top = 100;
mainLight.shadow.camera.bottom = -100;
mainLight.shadow.camera.left = -100;
mainLight.shadow.camera.right = 100;
scene.add(mainLight);

// Sunlight
const sunLight = new THREE.DirectionalLight(0xffe100, 0.8);
sunLight.position.set(-1, 1, 1).normalize();
scene.add(sunLight);

Data Visualization

Combine Three.js with visualization libraries like ECharts to display data in 3D scenes.

import * as echarts from 'echarts';

// Create ECharts instance
const chartDom = document.createElement('div');
document.body.appendChild(chartDom);
const chart = echarts.init(chartDom);

// Define data
const data = [
    // ...
];

// ECharts configuration
const option = {
    // ...
};

chart.setOption(option);

// Integrate ECharts with Three.js
const chartTexture = new THREE.CanvasTexture(chartDom);
const chartMaterial = new THREE.MeshBasicMaterial({ map: chartTexture, transparent: true });
const chartPlane = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), chartMaterial);
chartPlane.position.z = -5;
scene.add(chartPlane);

// Update texture when ECharts updates
chart.on('rendered', () => {
    chartTexture.needsUpdate = true;
});

Interior Design and Furniture Placement

In interior design scenes, focus on decoration, furniture model imports, and layout beyond basic modeling and lighting.

Importing Furniture Models

Use GLTFLoader or other loaders to import furniture models, adjusting position, rotation, and scale.

loader.load('path/to/furniture.glb', (gltf) => {
    const furniture = gltf.scene;
    furniture.scale.set(0.1, 0.1, 0.1); // Adjust model size
    furniture.position.set(2, 0, 2); // Set position
    furniture.rotation.y = Math.PI / 4; // Set rotation
    scene.add(furniture);
}, undefined, (error) => {
    console.error(error);
});

Indoor Lighting and Materials

Lighting and material choices are critical for ambiance. Use multiple light sources (point lights, spotlights, ambient light) to simulate natural and artificial lighting.

// Add ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// Add spotlight for chandelier effect
const spotLight = new THREE.SpotLight(0xffffff, 1.5, 100, Math.PI / 3);
spotLight.position.set(0, 5, 0);
spotLight.castShadow = true;
scene.add(spotLight);

// Use physical material for furniture realism
const furnitureMaterial = new THREE.MeshPhysicalMaterial({
    color: 0x64453b,
    metalness: 0.1,
    roughness: 0.7,
    clearcoat: 1,
    clearcoatRoughness: 0.2,
});

Interactivity and Navigation

Add interactivity like clicking to select furniture, dragging to reposition, or using OrbitControls for camera navigation.

// Use OrbitControls for camera navigation
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Smooth rotation
controls.target.set(0, 1, 0); // Set control center

// Click event handling (example)
window.addEventListener('click', onDocumentClick, false);
function onDocumentClick(event) {
    // Convert mouse coordinates to 3D space
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);

    // Check for clicked objects
    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
        const clickedObject = intersects[0].object;
        console.log("Clicked on", clickedObject.name);
        // Add more interaction logic (e.g., select, move)
    }
}

Performance Monitoring and Optimization

As scene complexity grows, performance optimization is crucial. Use Stats.js to monitor performance and apply LOD or instancing to reduce resource usage.

import * as Stats from 'stats.js';

const stats = new Stats();
document.body.appendChild(stats.dom);

// Update stats in animation loop
function animate() {
    requestAnimationFrame(animate);

    // Update scene, render, etc.

    // Update performance stats
    stats.update();
}

Advanced Materials and PBR Workflow

For enhanced realism, adopt a Physically Based Rendering (PBR) workflow. PBR materials like MeshStandardMaterial and MeshPhysicalMaterial simulate real-world light interactions, requiring metalness, roughness, ambient occlusion, and normal maps.

const material = new THREE.MeshStandardMaterial({
    map: textureLoader.load('path/to/diffuse.jpg'),
    metalnessMap: textureLoader.load('path/to/metalness.jpg'),
    roughnessMap: textureLoader.load('path/to/roughness.jpg'),
    normalMap: textureLoader.load('path/to/normal.jpg'),
    aoMap: textureLoader.load('path/to/ambientOcclusion.jpg'),
});

Particle Systems and Effects

Use particle systems for dynamic effects like smoke, fire, or rain to enhance scene liveliness.

const particleCount = 1000;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const material = new THREE.PointsMaterial({
    color: 0xffffff,
    size: 0.1,
    map: textureLoader.load('path/to/particleTexture.png'),
    blending: THREE.AdditiveBlending,
    transparent: true,
});

const particles = new THREE.Points(geometry, material);
scene.add(particles);

// Update particle positions
function animateParticles() {
    for (let i = 0; i < particleCount * 3; i += 3) {
        positions[i] += (Math.random() - 0.5) * 0.1;
        positions[i + 1] += (Math.random() - 0.5) * 0.1;
        positions[i + 2] += (Math.random() - 0.5) * 0.1;
    }
    geometry.attributes.position.needsUpdate = true;
}

Sound Integration

Sound enhances immersion. Use THREE.AudioListener and THREE.Audio for ambient or object-specific sound effects.

const listener = new THREE.AudioListener();
camera.add(listener);

const sound = new THREE.Audio(listener);
const audioLoader = new THREE.AudioLoader();
audioLoader.load('path/to/audio.mp3', (buffer) => {
    sound.setBuffer(buffer);
    sound.setLoop(true);
    sound.setVolume(0.5);
    sound.play();
});

Animation Sequences and Action Blending

For characters or complex animations, use AnimationMixer and AnimationAction to control sequences and blend multiple animations.

const mixer = new THREE.AnimationMixer(gltf.scene);

gltf.animations.forEach((animationClip) => {
    const action = mixer.clipAction(animationClip);
    action.play();
});

// Update mixer each frame
function updateMixer(deltaTime) {
    mixer.update(deltaTime);
}

UI Integration and Data Visualization

Use libraries like dat.GUI or custom HTML/CSS interfaces to control scene parameters, or integrate advanced visualization libraries like D3.js for data display.

const gui = new dat.GUI();
gui.add(material, 'metalness', 0, 1).name('Metalness');
gui.add(material, 'roughness', 0, 1).name('Roughness');

Three.js Project Practice Data Visualization

Three.js Data Visualization

Three.js enables data visualization by transforming 2D data into 3D graphics, enhancing data exploration and understanding. Below is an example of creating a 3D map visualization using Three.js and GeoJSON data.

First, ensure the necessary dependencies are installed, including three, three-geojson, and three-orbitcontrols:

npm install three three-geojson three-orbitcontrols

Create an HTML file to include Three.js and other libraries:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js Data Visualization</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three-geojson@0.0.1/build/three.geojson.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three-orbitcontrols@0.1.24/build/OrbitControls.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

Then, write the JavaScript code in app.js:

document.addEventListener('DOMContentLoaded', () => {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Add OrbitControls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);

    // Load GeoJSON data
    const geojsonLoader = new THREE.GeoJSONLoader();
    geojsonLoader.load('path/to/your/geojson/file.geojson', (data) => {
        const geojsonGeometry = data.geometry;
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
        const mesh = new THREE.Mesh(geojsonGeometry, material);
        scene.add(mesh);

        // Set initial camera position and orientation
        camera.position.setZ(500);
        camera.lookAt(geojsonGeometry.boundingSphere.center);
    });

    // Render loop
    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
        controls.update();
    }
    animate();
});

This code loads a GeoJSON file, converts it into a Three.js geometry, and creates a 3D mesh with a basic material. OrbitControls allows users to rotate, pan, and zoom the scene via mouse or touch.

Three.js and D3 Data Visualization

For more complex visualizations, combine Three.js with data processing libraries like D3.js, using custom geometries and color mappings.

First, install d3 and three:

npm install d3 three

Create an HTML file to include Three.js, D3.js, and your JavaScript file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js & D3.js Bar Chart</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdn.jsdelivr.net/npm/d3@7.0.4/dist/d3.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

Then, write the JavaScript code in app.js:

document.addEventListener('DOMContentLoaded', () => {
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Create bar chart data
    const data = [5, 20, 15, 25, 10];

    // Use D3.js to calculate max value and scale
    const maxDataValue = d3.max(data);
    const barWidth = 1;
    const barMargin = 0.1;
    const barCount = data.length;
    const barHeightScale = d3.scaleLinear().domain([0, maxDataValue]).range([0, 1]);

    // Create bar chart
    for (let i = 0; i < barCount; i++) {
        const barHeight = barHeightScale(data[i]);
        const barGeometry = new THREE.BoxGeometry(barWidth, barHeight, 1);
        const barMaterial = new THREE.MeshBasicMaterial({ color: 0x007bff });
        const barMesh = new THREE.Mesh(barGeometry, barMaterial);
        barMesh.position.set(i * (barWidth + barMargin), 0, 0);
        scene.add(barMesh);
    }

    // Set camera position
    camera.position.z = 5;

    // Render loop
    function animate() {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
    }
    animate();
});

This code uses D3.js to compute the maximum value and scale, then creates a bar for each data point using BoxGeometry, with heights based on data values. The camera is positioned to view the chart.

Visualization Interactivity

Color Mapping

Color mapping visually distinguishes data ranges. Map data values to colors for intuitive representation.

// Use D3 color scale
const colorScale = d3.scaleSequential(d3.interpolateViridis)
    .domain([d3.min(data), d3.max(data)]);

// Apply color mapping in bar creation
for (let i = 0; i < barCount; i++) {
    const barHeight = barHeightScale(data[i]);
    const color = new THREE.Color(colorScale(data[i]));
    const barMaterial = new THREE.MeshBasicMaterial({ color });
    // ... rest of the code unchanged
}

Interactive Tooltips

Display data values when hovering over bars by listening to mouse movement and calculating intersections.

// Add Raycaster and Mouse Vector
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// Listen for mouse movement
document.addEventListener('mousemove', onDocumentMouseMove, false);
function onDocumentMouseMove(event) {
    event.preventDefault();
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}

// Check for mouse-bar interactions in render loop
function animate() {
    // ... other rendering code
    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
        const intersect = intersects[0];
        // Display tooltip (simplified; real apps may need more logic)
        console.log(`Value: ${data[Math.floor(intersects[0].object.position.x)]}`);
    }
    // ... rendering complete
}

Dynamic Data Updates

Update data in real-time or periodically using setInterval or WebSocket, recalculating and redrawing the chart.

// Function to fetch new data (example)
function fetchNewData() {
    // ... data fetching logic
    return newDataArray;
}

// Periodically update data
setInterval(() => {
    const newData = fetchNewData();
    scene.remove(...scene.children); // Remove old bars
    data = newData; // Update data array
    // Recreate bars, same process as above
    for (let i = 0; i < data.length; i++) {
        // ... repeat bar creation process
    }
    // Re-render
    renderer.render(scene, camera);
}, 5000); // Update every 5 seconds

Lighting and Shadows

Add lighting to make 3D visuals more realistic, using ambient, point, or directional lights, and enable shadow casting/receiving.

// Add ambient light
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);

// Add main light
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1).normalize();
directionalLight.castShadow = true;
scene.add(directionalLight);

// Enable shadow casting for bars
for (const bar of scene.children) {
    if (bar instanceof THREE.Mesh) {
        bar.castShadow = true;
    }
}

Performance Optimization

As data volume and complexity increase, optimize performance with:

  • Reduce Draw Calls: Merge materials and geometries.
  • Use InstancedMesh: For many similar objects, use THREE.InstancedMesh.
  • LOD (Level of Detail): Use different detail levels based on camera distance.
  • Frustum Culling: Render only objects within the camera’s frustum.

Responsive Design

Ensure visualizations adapt to various screen sizes and devices by adjusting the camera’s aspect ratio, renderer size, and UI layout.

window.addEventListener('resize', () => {
    const width = window.innerWidth;
    const height = window.innerHeight;
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
    renderer.setSize(width, height);
});

Interaction Design

Beyond tooltips, add interactions like:

  • Click Events: Allow users to select bars for more info or actions.
  • Filtering: Provide tools to filter displayed data by attributes.
  • Animated Transitions: Smooth transitions for data updates or interactions.

WebGL2 and Post-Processing

If supported, use WebGL2 for advanced graphics features. Apply post-processing (e.g., depth of field, blur, color correction) to enhance visuals.

// Enable WebGL2
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });

// Post-processing
const composer = new POSTPROCESSING.EffectComposer(renderer);
const renderPass = new POSTPROCESSING.RenderPass(scene, camera);
const effectPass = new POSTPROCESSING.BloomEffect({ luminanceThreshold: 0.5 });
composer.addPass(renderPass);
composer.addPass(effectPass);

3D Charts and Data Visualization Applications

In modern data visualization, 3D charts and AR/VR technologies offer new dimensions and interaction methods. By combining Three.js’s powerful 3D rendering capabilities, D3.js’s data processing strengths, and WebXR API’s augmented and virtual reality experiences, we can create engaging and educational data visualization applications.

3D Charts with D3.js

Goal: Create a 3D scatter plot to visualize relationships in a dataset, using D3.js for data processing and color mapping.

// Import required libraries
import * as THREE from 'three';
import * as d3 from 'd3';

// Prepare data
const data = [ /*...*/ ]; // Assume an array of data with x, y, z coordinates

// Initialize scene, camera, and renderer
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Create scatter plot
data.forEach((point, index) => {
    const geometry = new THREE.SphereGeometry(0.1, 32, 32);
    const color = d3.interpolateCool(index / data.length); // Use D3 color interpolation
    const material = new THREE.MeshBasicMaterial({ color: new THREE.Color(color) });
    const sphere = new THREE.Mesh(geometry, material);
    sphere.position.set(point.x, point.y, point.z);
    scene.add(sphere);
});

// Render loop
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();

AR/VR with WebXR Integration

Goal: Bring the 3D scatter plot into an augmented reality environment, allowing users to view and interact with data in the real world.

// Import WebXR libraries
import { XR8 } from '@xr8/core';
import { Renderer, ThreejsRuntime } from '@xr8/threejs-runtime';

// Initialize WebXR runtime
const runtime = new ThreejsRuntime({
    canvas: renderer.domElement,
    camera,
    scene,
    renderer,
});

// Configure WebXR startup
const xrConfig = {
    requiredFeatures: ['hit-test'],
    domOverlay: { root: document.body },
};

// Start WebXR session
XR8.XrSessionManager.start(xrConfig, runtime.context).then(session => {
    // Adjust scene content for AR environment as needed
    // E.g., use hit-test results to position 3D objects in the real world
}).catch(err => console.error('Error starting WebXR session:', err));

This creates a 3D scatter plot and integrates it into an augmented reality environment via WebXR. Note that WebXR requires device support for the WebXR API and may need an HTTPS environment.

By combining D3.js’s data processing, Three.js’s 3D rendering, and WebXR’s AR/VR capabilities, we can create immersive, intuitive, and interactive data visualizations, opening new possibilities for data analysis and presentation. However, developers must focus on performance optimization, device compatibility, and user experience to ensure broad accessibility.

Enhanced Interactivity

To improve user experience, add interactive elements like click events, drag operations, zooming, and panning.

Click Events

Listen for mouse clicks in the 3D scene to identify selected objects and display related information.

// Add Raycaster
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

// Listen for mouse clicks
document.addEventListener('mousedown', onDocumentMouseDown, false);
function onDocumentMouseDown(event) {
    event.preventDefault();
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children);

    if (intersects.length > 0) {
        const selectedObject = intersects[0].object;
        // Display or process selected object
        console.log(`Selected object: ${selectedObject.name}`);
    }
}

Drag Operations

Use the OrbitControls library to enable scene rotation, panning, and zooming.

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// Create OrbitControls instance
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Smooth movement
controls.dampingFactor = 0.25;
controls.screenSpacePanning = false; // Pan in 3D space
controls.minDistance = 10;
controls.maxDistance = 1000;

// Update controls
function animate() {
    // ...
    controls.update();
    renderer.render(scene, camera);
}

Zooming and Panning

In WebXR environments, use XRInputSource select events to handle touch or controller interactions.

session.addEventListener('selectstart', (event) => {
    const hitTestResult = event.inputSource.hands[0].hitTest(event.selectPose.pose.position);
    if (hitTestResult) {
        const worldPosition = hitTestResult.getTransformedPosition(new THREE.Vector3());
        // Use worldPosition to update 3D object positions
    }
});

Data Updates and Real-Time Synchronization

Real-time data updates are common in visualization applications. Use WebSocket, Socket.io, or similar technologies for real-time data synchronization.

import io from 'socket.io-client';

// Connect to server
const socket = io('http://your-server-url');

// Update 3D chart when new data is received
socket.on('new-data', (newData) => {
    // Update data array
    data = newData;

    // Remove old 3D objects
    scene.remove(...scene.children);

    // Recreate 3D chart with new data
    // ... repeat 3D scatter plot creation code

    // Render updated scene
    renderer.render(scene, camera);
});

This approach creates highly interactive and real-time 3D data visualizations, enhanced with AR/VR for immersive experiences. Developers should also consider performance optimization, error handling, and user feedback to ensure stability and usability.

Animations and Transition Effects

Smooth animations and transitions greatly enhance user experience. Three.js offers built-in animation features like keyframe animations and timelines.

import { AnimationMixer, KeyframeTrack, Vector3 } from 'three';

// Create a color change keyframe track
const colorTrack = new KeyframeTrack(
    '.material.color',
    [0, 1], // Times
    [
        1, 0, 0, // Start color (red)
        0, 1, 0, // End color (green)
    ],
    THREE.LinearInterpolation
);

// Create an animation mixer
const mixer = new AnimationMixer(scene);

// Add track to mixer
const action = mixer.clipAction(colorTrack);
action.play();

// Update animation in render loop
function animate() {
    // ...
    mixer.update(delta);
    renderer.render(scene, camera);
}

WebGL Shaders

WebGL shaders allow custom rendering for complex visual effects. For example, create a shader that changes a 3D object’s color based on data values:

// Create a custom shader material
const vertexShader = `
    varying vec3 vWorldPosition;
    void main() {
        vec4 worldPosition = modelMatrix * vec4(position, 1.0);
        vWorldPosition = worldPosition.xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
`;

const fragmentShader = `
    uniform float uDataValue;
    varying vec3 vWorldPosition;
    void main() {
        vec3 color = vec3(uDataValue);
        gl_FragColor = vec4(color, 1.0);
    }
`;

const customMaterial = new THREE.ShaderMaterial({
    uniforms: {
        uDataValue: { value: 0.5 },
    },
    vertexShader,
    fragmentShader,
});

// Apply material to 3D object
const geometry = new THREE.SphereGeometry(0.1, 32, 32);
const sphere = new THREE.Mesh(geometry, customMaterial);
scene.add(sphere);

// Update shader data value
function updateDataValue(value) {
    customMaterial.uniforms.uDataValue.value = value;
}

Performance Optimization

For large datasets or complex 3D scenes, performance optimization is critical. Consider these strategies:

  • LOD (Level of Detail): Dynamically reduce detail based on distance from the camera.
  • Instancing: Use for rendering many identical or similar objects.
  • Batching: Combine multiple objects into a single geometry to reduce render calls.
  • Culling: Remove objects outside the camera’s view.
  • BufferGeometry: Use efficient data structures for geometry storage.

Overview

This document outlines the creation of advanced 3D data visualizations using Three.js, D3.js, and WebXR. By leveraging 3D charts, AR/VR integration, interactivity, real-time updates, animations, shaders, and optimization techniques, developers can build immersive and effective visualization applications. Attention to performance, compatibility, and user experience ensures broad accessibility and engagement.

Three.js and WebXR for Augmented Reality and Virtual Reality Development

Using Three.js with WebXR for Augmented Reality (AR) and Virtual Reality (VR) development enables the creation of highly immersive 3D interactive experiences. By incorporating spatial audio, users can perceive sound direction and distance, further enhancing immersion.

WebXR Integration

First, ensure that Three.js and WebXR-related libraries are included in your project:

<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/webxr/ARButton.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/jsm/webxr/VRButton.js"></script>

Initialize Three.js Scene

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

Add WebXR Entry Buttons

const arButton = ARButton.createButton(renderer);
document.body.appendChild(arButton);
const vrButton = VRButton.createButton(renderer);
document.body.appendChild(vrButton);

3D Interaction

Use OrbitControls or PointerLockControls to handle user interactions:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.screenSpacePanning = false;
controls.minDistance = 1;
controls.maxDistance = 1000;

Spatial Audio

Three.js provides a simple audio system for creating 3D sound sources and listeners:

const audioLoader = new THREE.AudioLoader();
const listener = new THREE.AudioListener();
camera.add(listener);

const sound = new THREE.PositionalAudio(listener);
audioLoader.load('path/to/audio.mp3', function(buffer) {
    sound.setBuffer(buffer);
    sound.setRefDistance(10); // Adjust sound propagation distance
});

// Add a 3D sound source object
const audioPosition = new THREE.Vector3(0, 0, -5);
const audioEmitter = new THREE.Object3D();
audioEmitter.position.copy(audioPosition);
scene.add(audioEmitter);
audioEmitter.add(sound);

WebXR Session Management

const xr = new XRSessionManager(renderer, {
    requiredFeatures: ['hit-test'], // For AR functionality
    optionalFeatures: ['hand-tracking'], // For hand tracking
});

xr.addEventListener('session-started', (event) => {
    // Adjust scene content for AR or VR environments as needed
});

xr.addEventListener('session-ended', (event) => {
    // Cleanup when user exits AR/VR mode
});

// Start XR session
xr.requestSession().then(session => {
    xr.setSession(session);
});

Render Loop

function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
    xr.update();
}
animate();

Collision Detection and Interaction Feedback

User interaction with 3D objects is central to AR/VR applications, and collision detection is key to enabling it. Three.js offers methods like Raycaster and physics engines such as Cannon.js or Ammo.js.

Raycaster Collision Detection

function onDocumentMouseMove(event) {
    event.preventDefault();
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(scene.children);
    if (intersects.length > 0) {
        const firstIntersection = intersects[0];
        // Handle collision, e.g., change color or play sound
        firstIntersection.object.material.color.set(0xff0000);
    }
}

Physics Engine Integration

For complex physical interactions, integrate a physics engine like Cannon.js:

import * as Cannon from 'cannon-es';

// Create physics world
const world = new Cannon.World();
world.gravity.set(0, -9.82, 0);

// Create physical representation of 3D object
const boxBody = new Cannon.Body({
    mass: 1,
    shape: new Cannon.Box(new Cannon.Vec3(1, 1, 1)),
});
boxBody.position.set(0, 2, 0);
world.addBody(boxBody);

// Update physics world
function updatePhysics() {
    world.step(1/60);
    boxMesh.position.copy(boxBody.position);
    boxMesh.quaternion.copy(boxBody.quaternion);
}

Performance Optimization and Device Adaptation

  • Performance Monitoring: Use browser developer tools’ Performance panel to identify bottlenecks.
  • Model Optimization: Reduce vertex counts, texture sizes, and use LOD techniques.
  • Rendering Optimization: Set appropriate render distances, avoid unnecessary rendering, and use instancing for many similar objects.
  • Device Adaptation: Detect device capabilities and provide resources tailored to different performance levels.

Cross-Platform Compatibility

Ensure the application runs smoothly across platforms and devices, especially optimizing for mobile devices by handling touch events and addressing performance constraints.

Testing and Deployment

Multi-User Collaboration and Network Synchronization

Allowing multiple users to collaborate in the same AR/VR environment is an appealing feature. This requires network synchronization to ensure all users see the same scene state.

Using WebSocket for Data Synchronization

Use WebSocket for real-time data exchange, reflecting each user’s actions instantly in all participants’ views.

import WebSocket from 'ws';

// Assume a WebSocket service endpoint
const socket = new WebSocket('ws://your-websocket-endpoint');

socket.addEventListener('open', () => {
    console.log('WebSocket connected');
});

socket.addEventListener('message', (event) => {
    const data = JSON.parse(event.data);
    // Update scene based on received data
    updateScene(data);
});

function sendData(data) {
    socket.send(JSON.stringify(data));
}

// Send updates when scene changes
function updateScene(data) {
    // Update local scene...
    sendData(data);
}

Implementing Simple Synchronization Logic

  • State Synchronization: Identify states to sync, such as object positions and rotations.
  • Conflict Resolution: Handle cases where multiple users modify the same object, using client prediction or server authority.
  • Latency Compensation: Account for network latency, possibly predicting user actions for smoother experiences.

Integrating Three.js into Existing Web Applications

Integrating Three.js in React

Creating a Three.js Component

Create a Three.js component using React’s useEffect hook to manage the component lifecycle, ensuring proper initialization and cleanup of the Three.js scene during mounting and unmounting.

import React, { useRef, useEffect } from 'react';
import * as THREE from 'three';

const ThreeScene = () => {
    const mountRef = useRef(null);

    useEffect(() => {
        let scene, camera, renderer;

        // Initialize scene
        const init = () => {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            renderer = new THREE.WebGLRenderer();
            renderer.setSize(window.innerWidth, window.innerHeight);
            mountRef.current.appendChild(renderer.domElement);

            // Add objects, lights, etc.
            const geometry = new THREE.BoxGeometry(1, 1, 1);
            const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
            const cube = new THREE.Mesh(geometry, material);
            scene.add(cube);

            camera.position.z = 5;
        };

        // Render loop
        const animate = () => {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };

        // Cleanup
        const cleanup = () => {
            renderer.dispose();
            mountRef.current.removeChild(renderer.domElement);
        };

        init();
        animate();

        return () => cleanup();
    }, []);

    return <div ref={mountRef} />;
};

export default ThreeScene;

Using in a React Application

Import and use the ThreeScene component in your React application.

import React from 'react';
import ThreeScene from './ThreeScene';

function App() {
    return (
        <div className="App">
            <h1>My 3D Scene</h1>
            <ThreeScene />
        </div>
    );
}

export default App;

Integrating Three.js in Vue

Creating a Three.js Component

In Vue, define a component and manage Three.js initialization and cleanup in the mounted and beforeDestroy lifecycle hooks.

<template>
  <div ref="container" class="three-container"></div>
</template>

<script>
import * as THREE from 'three';

export default {
  name: 'ThreeScene',
  mounted() {
    this.init();
  },
  beforeDestroy() {
    this.cleanup();
  },
  methods: {
    init() {
      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      const renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      this.$refs.container.appendChild(renderer.domElement);

      // Add objects, lights, etc.
      const geometry = new THREE.BoxGeometry(1, 1, 1);
      const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
      const cube = new THREE.Mesh(geometry, material);
      scene.add(cube);

      camera.position.z = 5;

      const animate = () => {
        requestAnimationFrame(animate);
        renderer.render(scene, camera);
      };
      animate();
    },
    cleanup() {
      const { container } = this.$refs;
      const renderer = container.querySelector('canvas');
      container.removeChild(renderer);
    },
  },
};
</script>

<style scoped>
.three-container {
  width: 100%;
  height: 100%;
}
</style>

Using in a Vue Application

Reference the component directly in your Vue application’s template.

<template>
  <div id="app">
    <h1>My 3D Scene</h1>
    <three-scene />
  </div>
</template>

<script>
import ThreeScene from './components/ThreeScene.vue';

export default {
  components: {
    ThreeScene,
  },
};
</script>

Integration Techniques and Challenges

State Management with React’s useReducer

In complex Three.js applications, managing multiple states (e.g., scene objects, lighting changes, animation parameters) may be necessary. In React, use useReducer instead of useState for more complex state logic.

const [state, dispatch] = useReducer(reducer, initialState);

function reducer(state, action) {
    switch (action.type) {
        case 'ADD_OBJECT':
            return { ...state, objects: [...state.objects, action.payload] };
        case 'UPDATE_LIGHT':
            return { ...state, lightIntensity: action.payload };
        // More state update logic...
        default:
            return state;
    }
}

Component Communication and Vuex in Vue

In Vue, if the Three.js component needs to share state with other parts of the application, use Vuex for state management. For example, control the visibility of scene objects or global light intensity.

// Define state in store
const state = {
    showObject: true,
    lightIntensity: 1,
};

const mutations = {
    setShowObject(state, value) {
        state.showObject = value;
    },
    setLightIntensity(state, value) {
        state.lightIntensity = value;
    },
};

// Use mapMutations in Three.js component
this.$store.commit('setShowObject', false);

Performance Optimization with React’s useMemo and Vue’s computed

To improve performance and avoid unnecessary renders, use React’s useMemo to cache computed results or Vue’s computed properties.

// React example
const transformedObject = useMemo(() => {
    return applyTransformations(object, transformationParams);
}, [object, transformationParams]);

// Vue example
computed: {
    transformedObject() {
        return applyTransformations(this.object, this.transformationParams);
    },
},

Modularity and Code Reuse

In both React and Vue, encapsulate Three.js-related logic (e.g., creating specific 3D objects, lights, animations) into reusable functions or components to facilitate maintenance and scalability.

TypeScript Support

For enhanced code robustness and maintainability, use TypeScript. Three.js provides official TypeScript type definitions, which can be installed and used.

npm install --save-dev @types/three

Then, leverage type checking in .tsx or .vue files.

Animation Library Integration

For complex animation needs, integrate third-party libraries like react-spring (React), vue-property-decorator (Vue), or use Three.js’s native animation system (e.g., AnimationMixer).

Responsive Layout and Adaptation

Ensure your Three.js scene adapts to different screen sizes and resolutions, especially on mobile devices. Use CSS media queries or framework-specific responsive APIs (e.g., React’s useEffect for window size changes, Vue’s @resize.window event) to adjust render size and camera parameters.

Share your love