第1章:光照与阴影——让数据“活”起来

大家好,我是老张。做3D数据可视化这些年,我最大的感触就是:没有光照的3D场景,就像没开灯的博物馆——再好的展品也看不清。

这一章,咱们聊聊光照与阴影。说白了,就是怎么给你的数据“打光”。

1.1 环境光(AmbientLight)——基础照明

环境光,你可以把它想象成阴天时的光线。它没有方向,均匀地照亮所有物体。

为什么需要它?

没有环境光,背光面会全黑。数据可视化里,我们不想丢失任何细节。

核心要点:环境光只改变亮度,不产生阴影,也没有方向感。

// 创建一个环境光,颜色偏暖,强度0.5
const ambientLight = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambientLight);

我个人习惯把环境光强度控制在0.3~0.6之间。太强了,其他光源的效果会被“冲淡”。

小技巧:环境光颜色可以跟主光源互补。比如主光源偏冷(蓝色),环境光就偏暖(橙色),这样画面更有层次。

1.2 平行光(DirectionalLight)——模拟太阳

平行光,顾名思义,所有光线都是平行的。它模拟的是太阳光。

特点:

  • 有明确的方向
  • 能产生清晰的阴影
  • 光照强度不随距离衰减

我在项目中遇到过一个问题:用平行光给一个城市建筑群打光,结果所有楼房的阴影方向都一样,看起来特别假。后来我加了两个平行光,一个主光,一个补光,效果才自然。

// 主光源:从右上角打下来
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);

// 辅助光:从左下角补光,强度低一些
const fillLight = new THREE.DirectionalLight(0xffeedd, 0.3);
fillLight.position.set(-10, 5, -10);
scene.add(fillLight);

注意:平行光的阴影范围需要手动设置。我曾经忘了设置,结果阴影只显示了一小块,排查了半天。

1.3 点光源(PointLight)——灯泡效果

点光源就像灯泡,从一个点向四面八方发光。它会产生柔和的阴影,光照强度随距离衰减。

适用场景:

  • 模拟路灯、台灯
  • 突出某个数据点
  • 营造氛围
const pointLight = new THREE.PointLight(0xffaa00, 1, 50);
pointLight.position.set(5, 3, 5);
scene.add(pointLight);

// 可以加个小球体表示光源位置,方便调试
const sphere = new THREE.Mesh(
  new THREE.SphereGeometry(0.2, 16, 16),
  new THREE.MeshBasicMaterial({ color: 0xffaa00 })
);
sphere.position.copy(pointLight.position);
scene.add(sphere);

你想想看,点光源的衰减参数很重要。默认是平方衰减,有时候太猛了。我一般把距离设成场景对角线长度的1.5倍,衰减系数调成1(线性衰减),这样过渡更自然。

1.4 聚光灯(SpotLight)——舞台追光

聚光灯有方向、有角度、有衰减范围。它就像一个手电筒,只照亮一个锥形区域。

参数说明:

参数 作用 我的建议值
angle 光锥角度(弧度) 0.3~0.6(约17°~34°)
penumbra 边缘柔化程度 0.2~0.5
decay 衰减速度 1(线性)或2(平方)
distance 最远照射距离 场景对角线长度
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 10, 0);
spotLight.target.position.set(0, 0, 0);
spotLight.angle = 0.4;
spotLight.penumbra = 0.3;
spotLight.decay = 1;
spotLight.distance = 30;

scene.add(spotLight);
scene.add(spotLight.target);

避坑指南:我曾经把聚光灯的angle设成0.1,结果只照亮了一个小点,还以为代码写错了。后来才意识到,角度太小了。做数据可视化,聚光灯适合突出某个关键数据区域,别用它做全局照明。

1.5 阴影投射与接收——让数据有“重量”

阴影是3D场景真实感的灵魂。没有阴影,物体就像飘在空中。

三步开启阴影:

  1. 渲染器开启阴影:renderer.shadowMap.enabled = true
  2. 光源设置投射阴影:light.castShadow = true
  3. 物体设置投射/接收:mesh.castShadow = true / mesh.receiveShadow = true
// 1. 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 柔和阴影

// 2. 光源
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;

// 3. 物体
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;

const plane = new THREE.Mesh(planeGeo, planeMat);
plane.receiveShadow = true;

性能警告:阴影很吃性能。我做过一个项目,场景里有200多个物体都开了阴影,结果帧率掉到15fps。后来只给关键物体开阴影,其他用假阴影(一个半透明圆片)代替,帧率回到了60fps。

1.6 实战组合拳

一个典型的数据可视化场景,我通常这样配光:

  • 环境光:强度0.3,颜色偏冷(0x404060)
  • 主平行光:强度1.0,从右上45°打下来,开启阴影
  • 补光:强度0.2,从左下打上来,不开阴影
  • 点光源:放在需要突出的数据点附近,强度0.5

嗯,这里要注意:光源数量不是越多越好。我见过有人一个场景放了20个光源,结果画面又亮又平,完全没有层次感。3~5个光源足够了。

调试技巧:lightHelpershadowCameraHelper可视化光源位置和阴影范围。Three.js官方有示例,直接复制过来用就行。

最后说一句:光照是门艺术,不是科学。同样的数据,不同的打光方式,传达的信息完全不同。多调、多看、多对比,慢慢就有感觉了。

下一章,咱们聊聊材质和纹理——怎么让数据表面有“质感”。