Three.js 实战案例 #

通过实战案例,将前面学习的知识应用到实际项目中,巩固所学技能。

案例一:3D 产品展示 #

功能需求 #

  • 360° 产品旋转展示
  • 鼠标拖拽旋转
  • 自动旋转
  • 材质切换
  • 缩放查看细节

完整代码 #

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

class ProductViewer {
  constructor(container) {
    this.container = container;
    this.autoRotate = true;
    this.currentMaterial = 0;
    
    this.init();
    this.loadModel();
    this.setupEventListeners();
    this.animate();
  }
  
  init() {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0xf5f5f5);
    
    const aspect = this.container.clientWidth / this.container.clientHeight;
    this.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
    this.camera.position.set(0, 1, 5);
    
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
    this.renderer.toneMappingExposure = 1.0;
    this.container.appendChild(this.renderer.domElement);
    
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    this.controls.dampingFactor = 0.05;
    this.controls.minDistance = 2;
    this.controls.maxDistance = 10;
    this.controls.maxPolarAngle = Math.PI / 2;
    
    this.setupLights();
    this.setupGround();
  }
  
  setupLights() {
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    this.scene.add(ambientLight);
    
    const mainLight = new THREE.DirectionalLight(0xffffff, 1);
    mainLight.position.set(5, 10, 5);
    mainLight.castShadow = true;
    mainLight.shadow.mapSize.width = 2048;
    mainLight.shadow.mapSize.height = 2048;
    mainLight.shadow.camera.near = 0.5;
    mainLight.shadow.camera.far = 50;
    mainLight.shadow.camera.left = -10;
    mainLight.shadow.camera.right = 10;
    mainLight.shadow.camera.top = 10;
    mainLight.shadow.camera.bottom = -10;
    this.scene.add(mainLight);
    
    const fillLight = new THREE.DirectionalLight(0xffffff, 0.3);
    fillLight.position.set(-5, 5, -5);
    this.scene.add(fillLight);
    
    const rimLight = new THREE.DirectionalLight(0xffffff, 0.5);
    rimLight.position.set(0, 5, -10);
    this.scene.add(rimLight);
  }
  
  setupGround() {
    const groundGeometry = new THREE.PlaneGeometry(20, 20);
    const groundMaterial = new THREE.ShadowMaterial({ opacity: 0.3 });
    this.ground = new THREE.Mesh(groundGeometry, groundMaterial);
    this.ground.rotation.x = -Math.PI / 2;
    this.ground.position.y = -0.5;
    this.ground.receiveShadow = true;
    this.scene.add(this.ground);
  }
  
  loadModel() {
    const loader = new GLTFLoader();
    
    loader.load(
      'product.glb',
      (gltf) => {
        this.model = gltf.scene;
        this.model.scale.set(1, 1, 1);
        this.model.position.set(0, 0, 0);
        
        this.model.traverse((child) => {
          if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
          }
        });
        
        this.scene.add(this.model);
        this.originalMaterials = this.getMaterials();
      },
      (progress) => {
        const percent = (progress.loaded / progress.total * 100).toFixed(2);
        console.log(`Loading: ${percent}%`);
      },
      (error) => {
        console.error('Error loading model:', error);
      }
    );
  }
  
  getMaterials() {
    const materials = {};
    this.model.traverse((child) => {
      if (child.isMesh && child.material) {
        materials[child.name] = child.material.clone();
      }
    });
    return materials;
  }
  
  changeMaterial(type) {
    if (!this.model) return;
    
    const materials = {
      gold: new THREE.MeshStandardMaterial({
        color: 0xffd700,
        metalness: 1.0,
        roughness: 0.2
      }),
      silver: new THREE.MeshStandardMaterial({
        color: 0xc0c0c0,
        metalness: 1.0,
        roughness: 0.3
      }),
      plastic: new THREE.MeshStandardMaterial({
        color: 0xff6b6b,
        metalness: 0.0,
        roughness: 0.5
      })
    };
    
    const newMaterial = materials[type];
    
    this.model.traverse((child) => {
      if (child.isMesh) {
        child.material = newMaterial;
      }
    });
  }
  
  resetMaterial() {
    if (!this.model || !this.originalMaterials) return;
    
    this.model.traverse((child) => {
      if (child.isMesh && this.originalMaterials[child.name]) {
        child.material = this.originalMaterials[child.name].clone();
      }
    });
  }
  
  setupEventListeners() {
    window.addEventListener('resize', () => this.onResize());
    
    this.controls.addEventListener('start', () => {
      this.autoRotate = false;
    });
  }
  
  onResize() {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(width, height);
  }
  
  animate() {
    requestAnimationFrame(() => this.animate());
    
    if (this.autoRotate && this.model) {
      this.model.rotation.y += 0.005;
    }
    
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }
}

const viewer = new ProductViewer(document.getElementById('container'));

案例二:3D 数据可视化 #

功能需求 #

  • 3D 柱状图展示
  • 数据动态更新
  • 鼠标悬停提示
  • 动画过渡效果

完整代码 #

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class DataVisualization {
  constructor(container) {
    this.container = container;
    this.data = [];
    this.bars = [];
    this.raycaster = new THREE.Raycaster();
    this.mouse = new THREE.Vector2();
    this.hoveredBar = null;
    
    this.init();
    this.setupEventListeners();
    this.animate();
  }
  
  init() {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x1a1a2e);
    
    const aspect = this.container.clientWidth / this.container.clientHeight;
    this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000);
    this.camera.position.set(15, 15, 15);
    
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
    this.container.appendChild(this.renderer.domElement);
    
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    
    this.setupLights();
    this.setupGrid();
  }
  
  setupLights() {
    const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
    this.scene.add(ambientLight);
    
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    directionalLight.position.set(10, 20, 10);
    this.scene.add(directionalLight);
  }
  
  setupGrid() {
    const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
    this.scene.add(gridHelper);
    
    const axesHelper = new THREE.AxesHelper(10);
    this.scene.add(axesHelper);
  }
  
  setData(data) {
    this.data = data;
    this.createBars();
  }
  
  createBars() {
    this.bars.forEach((bar) => this.scene.remove(bar));
    this.bars = [];
    
    const maxValue = Math.max(...this.data.map((d) => d.value));
    const barWidth = 0.8;
    const spacing = 1.5;
    const startX = -(this.data.length - 1) * spacing / 2;
    
    this.data.forEach((item, index) => {
      const height = (item.value / maxValue) * 10;
      
      const geometry = new THREE.BoxGeometry(barWidth, height, barWidth);
      geometry.translate(0, height / 2, 0);
      
      const color = new THREE.Color().setHSL(index / this.data.length, 0.7, 0.5);
      const material = new THREE.MeshStandardMaterial({
        color: color,
        metalness: 0.3,
        roughness: 0.7
      });
      
      const bar = new THREE.Mesh(geometry, material);
      bar.position.x = startX + index * spacing;
      bar.position.z = 0;
      bar.userData = { ...item, index };
      
      this.scene.add(bar);
      this.bars.push(bar);
    });
  }
  
  updateData(newData) {
    const maxValue = Math.max(...newData.map((d) => d.value));
    
    newData.forEach((item, index) => {
      if (this.bars[index]) {
        const targetHeight = (item.value / maxValue) * 10;
        const currentHeight = this.bars[index].geometry.parameters.height;
        
        this.animateBar(this.bars[index], currentHeight, targetHeight);
        this.bars[index].userData.value = item.value;
      }
    });
  }
  
  animateBar(bar, fromHeight, toHeight) {
    const duration = 500;
    const startTime = Date.now();
    
    const animate = () => {
      const elapsed = Date.now() - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const eased = 1 - Math.pow(1 - progress, 3);
      
      const currentHeight = fromHeight + (toHeight - fromHeight) * eased;
      
      bar.geometry.dispose();
      bar.geometry = new THREE.BoxGeometry(0.8, currentHeight, 0.8);
      bar.geometry.translate(0, currentHeight / 2, 0);
      
      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    };
    
    animate();
  }
  
  setupEventListeners() {
    window.addEventListener('resize', () => this.onResize());
    this.container.addEventListener('mousemove', (e) => this.onMouseMove(e));
  }
  
  onResize() {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(width, height);
  }
  
  onMouseMove(event) {
    const rect = this.container.getBoundingClientRect();
    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    
    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.bars);
    
    if (this.hoveredBar) {
      this.hoveredBar.material.emissive.setHex(0x000000);
    }
    
    if (intersects.length > 0) {
      this.hoveredBar = intersects[0].object;
      this.hoveredBar.material.emissive.setHex(0x333333);
      
      const data = this.hoveredBar.userData;
      this.showTooltip(event, `${data.label}: ${data.value}`);
    } else {
      this.hoveredBar = null;
      this.hideTooltip();
    }
  }
  
  showTooltip(event, text) {
    let tooltip = document.getElementById('tooltip');
    if (!tooltip) {
      tooltip = document.createElement('div');
      tooltip.id = 'tooltip';
      tooltip.style.cssText = `
        position: absolute;
        background: rgba(0, 0, 0, 0.8);
        color: white;
        padding: 8px 12px;
        border-radius: 4px;
        pointer-events: none;
        font-size: 14px;
      `;
      document.body.appendChild(tooltip);
    }
    
    tooltip.textContent = text;
    tooltip.style.left = event.clientX + 10 + 'px';
    tooltip.style.top = event.clientY + 10 + 'px';
    tooltip.style.display = 'block';
  }
  
  hideTooltip() {
    const tooltip = document.getElementById('tooltip');
    if (tooltip) {
      tooltip.style.display = 'none';
    }
  }
  
  animate() {
    requestAnimationFrame(() => this.animate());
    
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }
}

const viz = new DataVisualization(document.getElementById('container'));

viz.setData([
  { label: '一月', value: 120 },
  { label: '二月', value: 190 },
  { label: '三月', value: 300 },
  { label: '四月', value: 250 },
  { label: '五月', value: 280 },
  { label: '六月', value: 320 }
]);

案例三:粒子场景 #

功能需求 #

  • 大量粒子效果
  • 粒子动画
  • 鼠标交互
  • 性能优化

完整代码 #

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class ParticleScene {
  constructor(container) {
    this.container = container;
    this.particleCount = 5000;
    this.mouse = new THREE.Vector2();
    this.mouseWorld = new THREE.Vector3();
    
    this.init();
    this.createParticles();
    this.setupEventListeners();
    this.animate();
  }
  
  init() {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x000000);
    
    const aspect = this.container.clientWidth / this.container.clientHeight;
    this.camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
    this.camera.position.set(0, 0, 30);
    
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
    this.container.appendChild(this.renderer.domElement);
    
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    
    this.clock = new THREE.Clock();
  }
  
  createParticles() {
    const positions = new Float32Array(this.particleCount * 3);
    const colors = new Float32Array(this.particleCount * 3);
    const sizes = new Float32Array(this.particleCount);
    const randoms = new Float32Array(this.particleCount);
    
    for (let i = 0; i < this.particleCount; i++) {
      const i3 = i * 3;
      
      const radius = Math.random() * 20;
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos(Math.random() * 2 - 1);
      
      positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
      positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
      positions[i3 + 2] = radius * Math.cos(phi);
      
      const color = new THREE.Color();
      color.setHSL(Math.random(), 0.7, 0.5);
      colors[i3] = color.r;
      colors[i3 + 1] = color.g;
      colors[i3 + 2] = color.b;
      
      sizes[i] = Math.random() * 0.5 + 0.1;
      randoms[i] = Math.random();
    }
    
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    geometry.setAttribute('aColor', new THREE.BufferAttribute(colors, 3));
    geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1));
    geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1));
    
    const material = new THREE.ShaderMaterial({
      uniforms: {
        uTime: { value: 0 },
        uMouse: { value: new THREE.Vector3() },
        uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
      },
      vertexShader: `
        uniform float uTime;
        uniform vec3 uMouse;
        uniform float uPixelRatio;
        
        attribute vec3 aColor;
        attribute float aSize;
        attribute float aRandom;
        
        varying vec3 vColor;
        varying float vAlpha;
        
        void main() {
          vColor = aColor;
          
          vec3 pos = position;
          
          float angle = uTime * 0.2 * (1.0 + aRandom * 0.5);
          float cosA = cos(angle);
          float sinA = sin(angle);
          
          vec3 rotatedPos;
          rotatedPos.x = pos.x * cosA - pos.z * sinA;
          rotatedPos.y = pos.y + sin(uTime + aRandom * 6.28) * 0.5;
          rotatedPos.z = pos.x * sinA + pos.z * cosA;
          
          float dist = distance(rotatedPos, uMouse);
          float influence = smoothstep(5.0, 0.0, dist);
          rotatedPos += normalize(rotatedPos - uMouse) * influence * 2.0;
          
          vec4 mvPosition = modelViewMatrix * vec4(rotatedPos, 1.0);
          gl_Position = projectionMatrix * mvPosition;
          
          float sizeAttenuation = 300.0 / -mvPosition.z;
          gl_PointSize = aSize * sizeAttenuation * uPixelRatio;
          
          vAlpha = smoothstep(30.0, 5.0, -mvPosition.z);
        }
      `,
      fragmentShader: `
        varying vec3 vColor;
        varying float vAlpha;
        
        void main() {
          float dist = length(gl_PointCoord - vec2(0.5));
          if (dist > 0.5) discard;
          
          float alpha = 1.0 - smoothstep(0.2, 0.5, dist);
          alpha *= vAlpha;
          
          gl_FragColor = vec4(vColor, alpha);
        }
      `,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    });
    
    this.particles = new THREE.Points(geometry, material);
    this.scene.add(this.particles);
  }
  
  setupEventListeners() {
    window.addEventListener('resize', () => this.onResize());
    this.container.addEventListener('mousemove', (e) => this.onMouseMove(e));
  }
  
  onResize() {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(width, height);
    
    this.particles.material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2);
  }
  
  onMouseMove(event) {
    const rect = this.container.getBoundingClientRect();
    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    
    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(this.mouse, this.camera);
    
    const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
    raycaster.ray.intersectPlane(plane, this.mouseWorld);
  }
  
  animate() {
    requestAnimationFrame(() => this.animate());
    
    const elapsedTime = this.clock.getElapsedTime();
    
    this.particles.material.uniforms.uTime.value = elapsedTime;
    this.particles.material.uniforms.uMouse.value.copy(this.mouseWorld);
    
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }
}

const particleScene = new ParticleScene(document.getElementById('container'));

案例四:交互式地球 #

功能需求 #

  • 地球模型
  • 地点标记
  • 点击交互
  • 动画效果

完整代码 #

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class InteractiveGlobe {
  constructor(container) {
    this.container = container;
    this.locations = [];
    this.markers = [];
    this.raycaster = new THREE.Raycaster();
    this.mouse = new THREE.Vector2();
    
    this.init();
    this.createGlobe();
    this.createStars();
    this.setupEventListeners();
    this.animate();
  }
  
  init() {
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x000011);
    
    const aspect = this.container.clientWidth / this.container.clientHeight;
    this.camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
    this.camera.position.set(0, 0, 5);
    
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
    this.container.appendChild(this.renderer.domElement);
    
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.enableDamping = true;
    this.controls.minDistance = 3;
    this.controls.maxDistance = 10;
    this.controls.enablePan = false;
  }
  
  createGlobe() {
    const geometry = new THREE.SphereGeometry(1, 64, 64);
    
    const material = new THREE.MeshPhongMaterial({
      color: 0x1565c0,
      emissive: 0x072536,
      shininess: 10
    });
    
    this.globe = new THREE.Mesh(geometry, material);
    this.scene.add(this.globe);
    
    const atmosphereGeometry = new THREE.SphereGeometry(1.1, 64, 64);
    const atmosphereMaterial = new THREE.ShaderMaterial({
      vertexShader: `
        varying vec3 vNormal;
        void main() {
          vNormal = normalize(normalMatrix * normal);
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        varying vec3 vNormal;
        void main() {
          float intensity = pow(0.7 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
          gl_FragColor = vec4(0.3, 0.6, 1.0, 1.0) * intensity;
        }
      `,
      blending: THREE.AdditiveBlending,
      side: THREE.BackSide,
      transparent: true
    });
    
    const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
    this.scene.add(atmosphere);
    
    const ambientLight = new THREE.AmbientLight(0x333333);
    this.scene.add(ambientLight);
    
    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
    directionalLight.position.set(5, 3, 5);
    this.scene.add(directionalLight);
  }
  
  createStars() {
    const starsGeometry = new THREE.BufferGeometry();
    const starsCount = 2000;
    const positions = new Float32Array(starsCount * 3);
    
    for (let i = 0; i < starsCount; i++) {
      const radius = 50 + Math.random() * 50;
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos(Math.random() * 2 - 1);
      
      positions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
      positions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
      positions[i * 3 + 2] = radius * Math.cos(phi);
    }
    
    starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    
    const starsMaterial = new THREE.PointsMaterial({
      color: 0xffffff,
      size: 0.1
    });
    
    const stars = new THREE.Points(starsGeometry, starsMaterial);
    this.scene.add(stars);
  }
  
  addLocation(lat, lng, name) {
    const phi = (90 - lat) * (Math.PI / 180);
    const theta = (lng + 180) * (Math.PI / 180);
    
    const x = -Math.sin(phi) * Math.cos(theta);
    const y = Math.cos(phi);
    const z = Math.sin(phi) * Math.sin(theta);
    
    const markerGeometry = new THREE.SphereGeometry(0.02, 16, 16);
    const markerMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    const marker = new THREE.Mesh(markerGeometry, markerMaterial);
    
    marker.position.set(x, y, z);
    marker.userData = { name, lat, lng };
    
    this.globe.add(marker);
    this.markers.push(marker);
  }
  
  setupEventListeners() {
    window.addEventListener('resize', () => this.onResize());
    this.container.addEventListener('click', (e) => this.onClick(e));
  }
  
  onResize() {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(width, height);
  }
  
  onClick(event) {
    const rect = this.container.getBoundingClientRect();
    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
    
    this.raycaster.setFromCamera(this.mouse, this.camera);
    const intersects = this.raycaster.intersectObjects(this.markers);
    
    if (intersects.length > 0) {
      const marker = intersects[0].object;
      console.log('Clicked:', marker.userData.name);
      
      marker.material.color.setHex(0x00ff00);
      setTimeout(() => {
        marker.material.color.setHex(0xff0000);
      }, 500);
    }
  }
  
  animate() {
    requestAnimationFrame(() => this.animate());
    
    this.globe.rotation.y += 0.001;
    
    this.controls.update();
    this.renderer.render(this.scene, this.camera);
  }
}

const globe = new InteractiveGlobe(document.getElementById('container'));

globe.addLocation(39.9042, 116.4074, '北京');
globe.addLocation(40.7128, -74.0060, '纽约');
globe.addLocation(51.5074, -0.1278, '伦敦');
globe.addLocation(35.6762, 139.6503, '东京');
globe.addLocation(-33.8688, 151.2093, '悉尼');

总结 #

通过这些实战案例,你已经学会了:

  1. 产品展示:模型加载、材质切换、交互控制
  2. 数据可视化:动态图表、数据更新、动画过渡
  3. 粒子场景:GPU 粒子、着色器动画、鼠标交互
  4. 交互式地球:地理坐标转换、标记点、点击交互

继续实践和探索,创建更多精彩的 3D 应用!

资源推荐 #

最后更新:2026-03-28