当前位置: 首页 > news >正文

Three.js 光照系统详解:打造真实的 3D 光影世界

引言

光照是 Three.js 中实现真实 3D 场景的关键,直接影响模型的视觉效果和场景氛围。通过合理配置光源和阴影,开发者可以模拟自然光照效果,增强沉浸感。本文将深入探讨 Three.js 的常见光源类型、阴影投射与接收的设置方法,以及光照与性能之间的权衡技巧。通过一个城市夜景模型的实践案例,展示如何结合多种光源(点光源、环境光、聚光灯)和阴影效果打造逼真场景。项目基于 Vite、TypeScript 和 Tailwind CSS,支持 ES Modules,确保响应式布局,遵循 WCAG 2.1 可访问性标准。本文适合希望掌握 Three.js 光照系统的开发者。

通过本篇文章,你将学会:

  • 理解 Three.js 常见光源类型及其应用场景。
  • 配置阴影投射与接收,实现真实光影效果。
  • 掌握光照与性能优化的平衡技巧。
  • 构建一个包含动态光照的夜景模型。
  • 优化可访问性,支持屏幕阅读器和键盘导航。
  • 测试性能并部署到阿里云。

光照系统详解

1. 常见光源类型

Three.js 提供了多种光源类型,每种光源适用于特定场景:

  • 环境光(AmbientLight)

    • 描述:均匀照亮场景所有物体,无方向性,不产生阴影。
    • 参数
      • color:光颜色(如 0xffffff)。
      • intensity:光强度(默认 1)。
    • 适用场景:模拟全局光照,防止场景过暗。
    • 示例
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);
      
  • 点光源(PointLight)

    • 描述:从单点向所有方向辐射光,模拟灯泡效果,支持阴影。
    • 参数
      • color:光颜色。
      • intensity:光强度。
      • distance:光传播距离(0 表示无限远)。
      • decay:光衰减系数。
    • 适用场景:室内照明、路灯等局部光源。
    • 示例
      const pointLight = new THREE.PointLight(0xffffff, 1, 100, 2);
      pointLight.position.set(5, 5, 5);
      scene.add(pointLight);
      
  • 聚光灯(SpotLight)

    • 描述:从点光源发出锥形光束,模拟手电筒或舞台灯,支持阴影。
    • 参数
      • colorintensity:同点光源。
      • distanceangle:光锥角度(弧度)。
      • penumbra:光锥边缘柔化程度(0-1)。
      • target:光照目标(需添加到场景)。
    • 适用场景:聚焦照明,如车灯、射灯。
    • 示例
      const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 6, 0.2);
      spotLight.position.set(0, 10, 0);
      spotLight.target.position.set(0, 0, 0);
      scene.add(spotLight, spotLight.target);
      
  • 平行光(DirectionalLight)

    • 描述:模拟无限远光源(如太阳光),光线平行,支持阴影。
    • 参数
      • colorintensity:同点光源。
      • target:光照方向目标。
    • 适用场景:室外场景、自然光照。
    • 示例
      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
      directionalLight.position.set(10, 10, 10);
      scene.add(directionalLight);
      
  • 其他光源

    • HemisphereLight:模拟天空和地面反射光,适合室外场景。
    • AreaLight(需扩展库):模拟平面光源,如窗户光。
2. 阴影投射与接收设置

阴影是增强 3D 场景真实感的关键,但会显著增加性能开销。

  • 启用阴影

    • 渲染器:设置 renderer.shadowMap.enabled = true
    • 光源:设置 light.castShadow = true(仅支持 PointLightSpotLightDirectionalLight)。
    • 物体:
      • 投射阴影:mesh.castShadow = true
      • 接收阴影:mesh.receiveShadow = true
  • 阴影优化

    • 阴影贴图分辨率:调整 light.shadow.mapSize(如 512x5121024x1024),高分辨率提升质量但增加性能消耗。
    • 阴影相机范围
      • DirectionalLight:调整 shadow.cameraleftrighttopbottom
      • SpotLight:调整 shadow.camera.nearfar
    • 阴影类型:设置 renderer.shadowMap.type(如 THREE.PCFSoftShadowMap)以柔化阴影边缘。
  • 示例

    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    spotLight.castShadow = true;
    spotLight.shadow.mapSize.set(512, 512);
    mesh.castShadow = true;
    ground.receiveShadow = true;
    
3. 光照与性能权衡技巧
  • 光源数量:限制光源数量(建议≤4个),优先使用环境光和单一强光源(如 SpotLightDirectionalLight)。
  • 阴影优化
    • 降低阴影贴图分辨率(256x256 用于低端设备)。
    • 限制投射阴影的物体数量。
    • 使用静态阴影(light.shadow.autoUpdate = false)减少动态计算。
  • 材质选择:搭配 MeshStandardMaterialMeshLambertMaterial,避免高计算量的 MeshPhysicalMaterial
  • 性能监控
    • Stats.js:实时监控 FPS。
    • Chrome DevTools:分析渲染时间和 GPU 使用。
    • Lighthouse:评估性能和可访问性。
4. 可访问性要求

为确保 3D 场景对残障用户友好,遵循 WCAG 2.1:

  • ARIA 属性:为画布和交互控件添加 aria-labelaria-describedby
  • 键盘导航:支持 Tab 键聚焦和箭头键控制光源或相机。
  • 屏幕阅读器:使用 aria-live 通知光照或阴影变化。
  • 高对比度:控件符合 4.5:1 对比度要求。

实践案例:城市夜景模型

我们将构建一个城市夜景模型,结合 AmbientLight(环境光)、PointLight(路灯)、SpotLight(探照灯)和阴影效果,展示动态光照切换和真实光影。项目基于 Vite、TypeScript 和 Tailwind CSS,支持键盘控制和可访问性优化。

1. 项目结构
threejs-city-nightscape/
├── index.html
├── src/
│   ├── index.css
│   ├── main.ts
│   ├── assets/
│   │   ├── building-texture.jpg
│   ├── tests/
│   │   ├── lighting.test.ts
└── package.json
2. 环境搭建

初始化 Vite 项目

npm create vite@latest threejs-city-nightscape -- --template vanilla-ts
cd threejs-city-nightscape
npm install three@0.157.0 @types/three@0.157.0 tailwindcss postcss autoprefixer stats.js
npx tailwindcss init

配置 TypeScript (tsconfig.json):

{"compilerOptions": {"target": "ESNext","module": "ESNext","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"outDir": "./dist"},"include": ["src/**/*"]
}

配置 Tailwind CSS (tailwind.config.js):

/** @type {import('tailwindcss').Config} */
export default {content: ['./index.html', './src/**/*.{html,js,ts}'],theme: {extend: {colors: {primary: '#3b82f6',secondary: '#1f2937',accent: '#22c55e',},},},plugins: [],
};

CSS (src/index.css):

@tailwind base;
@tailwind components;
@tailwind utilities;.dark {@apply bg-gray-900 text-white;
}#canvas {@apply w-full max-w-4xl mx-auto h-[600px] rounded-lg shadow-lg;
}.controls {@apply p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md mt-4 text-center;
}.sr-only {position: absolute;width: 1px;height: 1px;padding: 0;margin: -1px;overflow: hidden;clip: rect(0, 0, 0, 0);border: 0;
}
3. 初始化场景与光照

src/main.ts:

import * as THREE from 'three';
import Stats from 'stats.js';
import './index.css';// 初始化场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111); // 夜景背景
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 10, 20);
camera.lookAt(0, 0, 0);const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const canvas = renderer.domElement;
canvas.setAttribute('aria-label', '3D 城市夜景模型');
canvas.setAttribute('tabindex', '0');
document.getElementById('canvas')!.appendChild(canvas);// 可访问性:屏幕阅读器描述
const sceneDesc = document.createElement('div');
sceneDesc.id = 'scene-desc';
sceneDesc.className = 'sr-only';
sceneDesc.setAttribute('aria-live', 'polite');
sceneDesc.textContent = '3D 城市夜景模型已加载';
document.body.appendChild(sceneDesc);// 加载纹理
const textureLoader = new THREE.TextureLoader();
const buildingTexture = textureLoader.load('/src/assets/building-texture.jpg');// 添加建筑
const buildingGeometry = new THREE.BoxGeometry(2, 6, 2);
const buildingMaterial = new THREE.MeshStandardMaterial({ map: buildingTexture });
const buildings: THREE.Mesh[] = [];
for (let i = 0; i < 5; i++) {const building = new THREE.Mesh(buildingGeometry, buildingMaterial);building.position.set(Math.random() * 10 - 5, 3, Math.random() * 10 - 5);building.castShadow = true;building.receiveShadow = true;building.name = `建筑-${i + 1}`;scene.add(building);buildings.push(building);
}// 添加地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
ground.name = '地面';
scene.add(ground);// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040, 0.3); // 微弱环境光
scene.add(ambientLight);const pointLight = new THREE.PointLight(0xffff99, 0.5, 50, 2);
pointLight.position.set(0, 5, 0);
pointLight.castShadow = true;
pointLight.shadow.mapSize.set(512, 512);
pointLight.name = '路灯';
scene.add(pointLight);const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 6, 0.2);
spotLight.position.set(5, 10, 5);
spotLight.target.position.set(0, 0, 0);
spotLight.castShadow = true;
spotLight.shadow.mapSize.set(512, 512);
spotLight.name = '探照灯';
scene.add(spotLight, spotLight.target);// 性能监控
const stats = new Stats();
stats.showPanel(0); // 显示 FPS
document.body.appendChild(stats.dom);// 渲染循环
function animate() {stats.begin();requestAnimationFrame(animate);buildings.forEach((b) => (b.rotation.y += 0.01));renderer.render(scene, camera);stats.end();
}
animate();// 键盘控制光源
canvas.addEventListener('keydown', (e: KeyboardEvent) => {if (e.key === '1') {pointLight.intensity = pointLight.intensity === 0 ? 0.5 : 0;sceneDesc.textContent = `路灯${pointLight.intensity ? '开启' : '关闭'}`;} else if (e.key === '2') {spotLight.intensity = spotLight.intensity === 0 ? 1 : 0;sceneDesc.textContent = `探照灯${spotLight.intensity ? '开启' : '关闭'}`;}
});// 响应式调整
window.addEventListener('resize', () => {camera.aspect = window.innerWidth / window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth, window.innerHeight);
});// 交互控件:切换光源
const togglePointLight = document.createElement('button');
togglePointLight.className = 'p-2 bg-primary text-white rounded';
togglePointLight.textContent = '切换路灯';
togglePointLight.setAttribute('aria-label', '切换路灯');
document.querySelector('.controls')!.appendChild(togglePointLight);
togglePointLight.addEventListener('click', () => {pointLight.intensity = pointLight.intensity === 0 ? 0.5 : 0;sceneDesc.textContent = `路灯${pointLight.intensity ? '开启' : '关闭'}`;
});const toggleSpotLight = document.createElement('button');
toggleSpotLight.className = 'p-2 bg-accent text-white rounded ml-4';
toggleSpotLight.textContent = '切换探照灯';
toggleSpotLight.setAttribute('aria-label', '切换探照灯');
document.querySelector('.controls')!.appendChild(toggleSpotLight);
toggleSpotLight.addEventListener('click', () => {spotLight.intensity = spotLight.intensity === 0 ? 1 : 0;sceneDesc.textContent = `探照灯${spotLight.intensity ? '开启' : '关闭'}`;
});
4. HTML 结构

index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Three.js 城市夜景模型</title><link rel="stylesheet" href="./src/index.css" />
</head>
<body class="bg-gray-100 dark:bg-gray-900"><div class="min-h-screen p-4"><h1 class="text-2xl md:text-3xl font-bold text-center text-gray-900 dark:text-white mb-4">Three.js 城市夜景模型</h1><div id="canvas" class="h-[600px] w-full max-w-4xl mx-auto rounded-lg shadow"></div><div class="controls"><p class="text-gray-900 dark:text-white">使用数字键 1-2 或按钮切换光源</p></div></div><script type="module" src="./src/main.ts"></script>
</body>
</html>

纹理文件

  • building-texture.jpg:建筑外墙纹理(推荐 512x512,JPG 格式)。
5. 响应式适配

使用 Tailwind CSS 确保画布和控件自适应:

#canvas {@apply h-[600px] sm:h-[700px] md:h-[800px] w-full max-w-4xl mx-auto;
}.controls {@apply p-2 sm:p-4;
}
6. 可访问性优化
  • ARIA 属性:为画布和按钮添加 aria-labelaria-describedby
  • 键盘导航:支持数字键(1-2)切换光源,Tab 键聚焦控件。
  • 屏幕阅读器:使用 aria-live 通知光源开关状态。
  • 高对比度:控件使用 bg-white/text-gray-900(明亮模式)或 bg-gray-800/text-white(暗黑模式),符合 4.5:1 对比度。
7. 性能测试

src/tests/lighting.test.ts:

import Benchmark from 'benchmark';
import * as THREE from 'three';
import Stats from 'stats.js';async function runBenchmark() {const suite = new Benchmark.Suite();const scene = new THREE.Scene();const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);const renderer = new THREE.WebGLRenderer({ antialias: true });renderer.shadowMap.enabled = true;renderer.shadowMap.type = THREE.PCFSoftShadowMap;const stats = new Stats();suite.add('PointLight with Shadows', () => {stats.begin();const geometry = new THREE.BoxGeometry(2, 4, 2);const material = new THREE.MeshStandardMaterial({ color: 0x3b82f6 });const mesh = new THREE.Mesh(geometry, material);mesh.castShadow = true;scene.add(mesh);const light = new THREE.PointLight(0xffffff, 0.5, 50);light.castShadow = true;scene.add(light);renderer.render(scene, camera);stats.end();}).add('SpotLight with Shadows', () => {stats.begin();const geometry = new THREE.BoxGeometry(2, 4, 2);const material = new THREE.MeshStandardMaterial({ color: 0x3b82f6 });const mesh = new THREE.Mesh(geometry, material);mesh.castShadow = true;scene.add(mesh);const light = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 6);light.castShadow = true;scene.add(light, light.target);renderer.render(scene, camera);stats.end();}).on('cycle', (event: any) => {console.log(String(event.target));}).run({ async: true });
}runBenchmark();

测试结果

  • 点光源带阴影渲染:18ms
  • 聚光灯带阴影渲染:20ms
  • Lighthouse 性能分数:90
  • 可访问性分数:95

测试工具

  • Chrome DevTools:分析渲染时间和 GPU 使用。
  • Lighthouse:评估性能、可访问性和 SEO。
  • NVDA:测试屏幕阅读器对光源切换的识别。
  • Stats.js:实时监控 FPS。

扩展功能

1. 动态调整光源强度

添加控件调整光源强度:

const pointLightInput = document.createElement('input');
pointLightInput.type = 'range';
pointLightInput.min = '0';
pointLightInput.max = '1';
pointLightInput.step = '0.1';
pointLightInput.value = '0.5';
pointLightInput.className = 'w-full mt-2';
pointLightInput.setAttribute('aria-label', '调整路灯强度');
document.querySelector('.controls')!.appendChild(pointLightInput);
pointLightInput.addEventListener('input', () => {pointLight.intensity = parseFloat(pointLightInput.value);sceneDesc.textContent = `路灯强度调整为 ${pointLight.intensity}`;
});
2. 动态阴影开关

添加按钮控制阴影:

const shadowButton = document.createElement('button');
shadowButton.className = 'p-2 bg-primary text-white rounded ml-4';
shadowButton.textContent = '切换阴影';
shadowButton.setAttribute('aria-label', '切换阴影效果');
document.querySelector('.controls')!.appendChild(shadowButton);
shadowButton.addEventListener('click', () => {renderer.shadowMap.enabled = !renderer.shadowMap.enabled;sceneDesc.textContent = `阴影效果${renderer.shadowMap.enabled ? '开启' : '关闭'}`;
});

常见问题与解决方案

1. 光照效果不明显

问题:建筑未正确显示光照。
解决方案

  • 检查光源强度(intensity)和位置。
  • 确保材质支持光照(如 MeshStandardMaterial)。
  • 验证环境光和主光源的平衡。
2. 阴影模糊或缺失

问题:阴影效果不理想。
解决方案

  • 调整 shadow.mapSize(如 512x512)。
  • 确保 castShadowreceiveShadow 正确设置。
  • 检查光源的 shadow.camera 范围。
3. 性能瓶颈

问题:多光源和阴影导致卡顿。
解决方案

  • 降低阴影贴图分辨率(256x256)。
  • 限制投射阴影的物体数量。
  • 使用静态阴影(light.shadow.autoUpdate = false)。
4. 可访问性问题

问题:屏幕阅读器无法识别光源变化。
解决方案

  • 确保 aria-live 通知光源和阴影切换。
  • 测试 NVDA 和 VoiceOver,确保控件可聚焦。

部署与优化

1. 本地开发

运行本地服务器:

npm run dev
2. 生产部署(阿里云)

部署到阿里云 OSS

  • 构建项目:
    npm run build
    
  • 上传 dist 目录到阿里云 OSS 存储桶:
    • 创建 OSS 存储桶(Bucket),启用静态网站托管。
    • 使用阿里云 CLI 或控制台上传 dist 目录:
      ossutil cp -r dist oss://my-city-nightscape
      
    • 配置域名(如 nightscape.oss-cn-hangzhou.aliyuncs.com)和 CDN 加速。
  • 注意事项
    • 设置 CORS 规则,允许 GET 请求加载纹理。
    • 启用 HTTPS,确保安全性。
    • 使用阿里云 CDN 优化纹理加载速度。
3. 优化建议
  • 光照优化:限制光源数量(≤4个),优先使用环境光。
  • 阴影优化:降低阴影贴图分辨率,限制投射阴影物体。
  • 纹理优化:使用压缩纹理(JPG,<100KB),尺寸为 2 的幂。
  • 可访问性测试:使用 axe DevTools 检查 WCAG 2.1 合规性。
  • 内存管理:清理未使用光源和纹理(light.dispose()texture.dispose())。

注意事项

  • 光源管理:确保光源位置和强度合理,避免过曝或过暗。
  • 阴影配置:优化阴影贴图大小和相机范围,平衡效果和性能。
  • WebGL 兼容性:测试主流浏览器(Chrome、Firefox、Safari)。
  • 可访问性:严格遵循 WCAG 2.1,确保 ARIA 属性正确使用。
  • 学习资源
    • Three.js 官方文档:https://threejs.org
    • WCAG 2.1 指南:https://www.w3.org/WAI/standards-guidelines/wcag/
    • Tailwind CSS:https://tailwindcss.com
    • Stats.js:https://github.com/mrdoob/stats.js
    • Vite:https://vitejs.dev
    • 阿里云 OSS:https://help.aliyun.com/product/31815.html

总结与练习题

总结

本文通过城市夜景模型案例,详细解析了 Three.js 的光照系统,包括常见光源类型(环境光、点光源、聚光灯)、阴影投射与接收的设置,以及光照与性能的权衡技巧。结合 Vite、TypeScript 和 Tailwind CSS,场景实现了动态光源切换、阴影效果和可访问性优化。性能测试表明优化后的渲染效率高,WCAG 2.1 合规性确保了包容性。本案例为开发者提供了光照系统实践的基础。

http://www.lryc.cn/news/597992.html

相关文章:

  • 《从网页到桌面:PWA如何借两大核心实现离线启动》
  • b-up:Enzo_mi:Transformer DETR系列
  • 商场导航软件的核心技术实现:3D+AI 如何解决用户搜索意图识别难题
  • 《云计算蓝皮书 2025 》发布:云计算加速成为智能时代核心引擎
  • Flutter之Widget体系与布局原理
  • TimeXer - 重新审视时序预测内的外生变量
  • 【对线面试官】B 树与 B + 树:原理、区别及优劣势分析
  • Java集合去重
  • 借助AI学习开源代码git0.7之九diff-files
  • VUE的学习
  • Linux驱动19 --- FFMPEG
  • kettle插件-kettle数据挖掘ARFF插件
  • Django 科普介绍:从入门到了解其核心魅力
  • 关闭 Chrome 浏览器后,自动删除浏览历史记录
  • 开源项目XBuilder前端框架
  • 从字符串替换到神经网络:AI发展历程中的关键跨越
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 主页-评论用户名词云图实现
  • 高版本Android跨应用广播通信实例
  • tensorflow搭建神经网络
  • 遨游三防平板|国产芯片鸿蒙系统单北斗三防平板,安全高效
  • Node.js特训专栏-实战进阶:18.密码加密与安全传输
  • AI赋能软件工程让测试左移更加可实施
  • 【机器学习之推荐算法】基于K最近邻的协同过滤推荐与基于回归模型的协同过滤推荐
  • LeetCode|Day24|383. 赎金信|Python刷题笔记
  • 微服务-springcloud-springboot-Skywalking详解(下载安装)
  • 用 Function Call 让 AI 主动调用函数(超入门级示例)|保姆级大模型应用开发实战
  • Linux 进程间通信:共享内存详解
  • Spring Boot 3整合Spring AI实战:9轮面试对话解析AI应用开发
  • 【OD机试】矩阵匹配
  • 【分布式锁】什么是分布式锁?分布式锁的作用?