东西都放好了,为什么看起来像塑料?

前言
前几篇把工程骨架、坐标体系、模型加载都搞定了。房间里有桌子、椅子、沙发、台灯,结构上看起来像那么回事了。
但跑起来一看,总觉得哪里不对:东西都有,就是看起来特别假,像塑料玩具。
我一开始以为是模型质量不好,换了几个模型还是一样。后来调了半天灯光,才发现问题不在模型,也不在灯光,而是材质和灯光配合的方式不对。
3D 渲染里有个基本常识:你看到的颜色,不是物体本身的颜色,而是物体表面反射出来的光。材质决定了表面怎么反射光,灯光决定了有多少光打在表面上。这两个东西是配合工作的,单独调哪个都没用。
这一篇就解决这个问题:为什么同一个沙发模型,别人做出来像真的布料,我做出来像塑料?材质、纹理、灯光之间到底是怎么配合的。
一、先搞清楚:你看到的颜色是怎么来的
很多人刚开始做 3D 的时候,以为材质就是"给物体上个色":沙发是灰色的,桌子是木色的,墙是白色的。
但真实的渲染不是这么回事。打个比方:
- 晚上关灯的时候,你看不到任何东西,不是因为物体没有颜色,是没有光
- 用红色灯光照白墙,墙就变成红色的
- 粗糙的墙看起来是哑光的,光滑的金属看起来是亮的
这就是材质和灯光配合的结果。材质描述的是"这个表面怎么反射光",不是"这个表面是什么颜色"。
先把基础概念列清楚:
| 概念 | 作用 | 举个例子 |
|---|---|---|
| 漫反射(Diffuse) | 光线打到表面后,向各个方向反射 | 墙面、布料,看起来哑光 |
| 高光(Specular) | 光线像镜子一样反射出去 | 金属、玻璃,有亮斑 |
| 粗糙度(Roughness) | 表面有多粗糙 | 粗糙的漫反射多,光滑的高光强 |
| 环境光(Ambient) | 整个场景的基础亮度 | 阴影里也不会全黑 |
| 主光源(Key Light) | 主要的照明方向 | 太阳、主台灯 |
很多初学者只加一个方向光,强度拉满,结果整个场景平得像一张纸,没有明暗层次。就是因为只考虑了主光源,没考虑环境光和材质的反射特性。
二、第一版:只有颜色的材质
先写第一版的材质设置,看看为什么看起来像塑料:
typescript
import { scene } from '@kit.ArkGraphics3DKit';
export class MaterialManager {
/**
* 给家具设置基础材质
*/
setupBasicMaterial(node: scene.Node, color: {r: number, g: number, b: number}): void {
const material = node.createComponent(scene.NodeComponentType.MATERIAL) as scene.Material;
// 只设置了基础颜色
material.albedo = color;
console.info('MaterialManager: basic material set', color);
}
}
然后调用它:
typescript
// 沙发设成灰色
materialMgr.setupBasicMaterial(sofaNode, {r: 0.6, g: 0.6, b: 0.6});
// 桌子设成木色
materialMgr.setupBasicMaterial(deskNode, {r: 0.5, g: 0.35, b: 0.2});
这个代码跑起来,东西是有颜色的,但看起来就是塑料感。为什么?因为只有漫反射颜色,没有粗糙度,没有高光,没有纹理。所有东西都像塑料球一样,表面光滑均匀。
这就是塑料感的来源:表面太"完美"了,没有任何细节,没有粗糙度变化,没有纹理。

三、加纹理和粗糙度,马上就不一样了
改进一下:给材质加上纹理和粗糙度参数。
typescript
export class MaterialManager {
/**
* 设置布料材质
*/
setupFabricMaterial(node: scene.Node, texturePath: string): void {
const material = node.createComponent(scene.NodeComponentType.MATERIAL) as scene.Material;
// 基础颜色
material.albedo = {r: 0.6, g: 0.6, b: 0.6};
// 加载纹理贴图
material.loadAlbedoTexture(texturePath);
// 布料是粗糙的,粗糙度设高一点
material.roughness = 0.8;
// 布料没有金属感,金属度设0
material.metallic = 0.0;
console.info('MaterialManager: fabric material set');
}
/**
* 设置木质材质
*/
setupWoodMaterial(node: scene.Node, texturePath: string): void {
const material = node.createComponent(scene.NodeComponentType.MATERIAL) as scene.Material;
material.albedo = {r: 0.5, g: 0.35, b: 0.2};
material.loadAlbedoTexture(texturePath);
// 木头有一点光泽,但不是金属
material.roughness = 0.6;
material.metallic = 0.0;
console.info('MaterialManager: wood material set');
}
/**
* 设置金属材质
*/
setupMetalMaterial(node: scene.Node): void {
const material = node.createComponent(scene.NodeComponentType.MATERIAL) as scene.Material;
material.albedo = {r: 0.8, g: 0.8, b: 0.8};
// 金属很光滑,金属度高
material.roughness = 0.2;
material.metallic = 0.9;
console.info('MaterialManager: metal material set');
}
}
现在沙发是布料材质,粗糙度 0.8,看起来就哑光了;桌子是木头,粗糙度 0.6,有一点自然的光泽;台灯支架是金属,粗糙度 0.2,高光就出来了。
这时候再看场景,塑料感就少了很多。因为不同材质有不同的反射特性,不再是所有东西都一个样子。

四、灯光不是越多越好
材质调好了,接下来调灯光。很多人以为灯光越多越亮越好,结果加了五六个方向光,场景更假了。
灯光太多有两个问题:
- 所有阴影都被照亮了,没有明暗层次,像个白板
- 渲染性能下降,每个光源都要算一遍光照
正确的做法是分层次布光:
| 灯光类型 | 作用 | 数量 | 例子 |
|---|---|---|---|
| 主光源(Key Light) | 主要照明,决定方向和阴影 | 1个 | 从窗户照进来的自然光 |
| 补光(Fill Light) | 照亮阴影区域,不要全黑 | 1个 | 环境反射的光 |
| 轮廓光(Rim Light) | 从背后打光,突出物体边缘 | 可选 | 让物体从背景里分离出来 |
| 环境光(Ambient) | 整体基础亮度 | 1个 | 天空和地面反射的环境光 |
写个 LightManager:
typescript
export class LightManager {
setupSceneLights(scene: scene.Scene): void {
// 1. 主光源:从斜上方照下来
const keyLight = scene.createNode('KeyLight');
const keyLightComp = keyLight.createComponent(scene.NodeComponentType.LIGHT) as scene.Light;
keyLightComp.lightType = scene.LightType.DIRECTIONAL;
keyLightComp.intensity = 1.2; // 主光源最亮
keyLight.position = {x: 5, y: 8, z: 3};
keyLight.rotation = {x: -45, y: 30, z: 0};
scene.getRoot().addChild(keyLight);
// 2. 补光:从另一侧补一点
const fillLight = scene.createNode('FillLight');
const fillLightComp = fillLight.createComponent(scene.NodeComponentType.LIGHT) as scene.Light;
fillLightComp.lightType = scene.LightType.DIRECTIONAL;
fillLightComp.intensity = 0.4; // 补光弱一点
fillLight.position = {x: -3, y: 4, z: -2};
scene.getRoot().addChild(fillLight);
// 3. 环境光:整体基础亮度
const ambientLight = scene.createNode('AmbientLight');
const ambientComp = ambientLight.createComponent(scene.NodeComponentType.LIGHT) as scene.Light;
ambientComp.lightType = scene.LightType.AMBIENT;
ambientComp.intensity = 0.3; // 环境光不要太亮,不然阴影没了
scene.getRoot().addChild(ambientLight);
console.info('LightManager: 3 lights setup done');
}
}
主光源 1.2,补光 0.4,环境光 0.3。这个比例大概能做出自然的明暗层次:主光面亮,背光面有补光不会全黑,环境光保证最暗的地方也能看到细节。

五、做几个预设模式:日间、暖光、展示
真实应用里,用户可能想要不同的灯光氛围:白天模式、晚上暖光模式、展示模式。
做一个 SceneThemeManager,提供几个预设:
typescript
export class SceneThemeManager {
// 日间模式
applyDaylight(scene: scene.Scene): void {
// 冷色调,亮度高
this.setLightColor(scene, {r: 1.0, g: 1.0, b: 1.0});
this.setLightIntensity(scene, {key: 1.5, fill: 0.5, ambient: 0.4});
}
// 暖光模式(晚上)
applyWarmLight(scene: scene.Scene): void {
// 暖黄色,亮度低
this.setLightColor(scene, {r: 1.0, g: 0.9, b: 0.7});
this.setLightIntensity(scene, {key: 0.8, fill: 0.3, ambient: 0.2});
}
// 展示模式(产品展示用)
applyShowcase(scene: scene.Scene): void {
// 均匀照亮,突出产品
this.setLightColor(scene, {r: 1.0, g: 1.0, b: 1.0});
this.setLightIntensity(scene, {key: 1.0, fill: 0.8, ambient: 0.5});
}
}
这样用户可以一键切换场景氛围,不用手动调每个灯光参数。
六、几个最容易踩的质感坑
把这一篇遇到的坑总结一下:
| 坑 | 现象 | 解决办法 |
|---|---|---|
| 只有基础颜色,没有粗糙度 | 所有东西都像塑料 | 不同材质设不同粗糙度,布料高、金属低 |
| 灯光越多越好 | 场景平得像白板,没有阴影 | 分层布光:1主光+1补光+1环境光就够 |
| 环境光太亮 | 阴影全没了,没有立体感 | 环境光别太亮,0.2~0.4差不多 |
| 纹理分辨率太高 | 内存涨,加载慢 | 家具纹理不用4K,1024足够 |
| 白色家具高光过曝 | 白家具一块死白 | 降低曝光,或者把粗糙度调高一点 |
| 所有材质都一个样 | 整个场景材质没有区分 | 布料、木头、金属、玻璃分别设参数 |
这些问题都不是 API 用错了,是对"材质+灯光"怎么配合没有概念。3D 渲染本质上是在模拟物理世界的光传播,不是简单地给物体上个颜色。
总结
第四篇的核心就一句话:质感不是靠模型好,是靠材质和灯光配合出来的。
- 材质描述的是表面怎么反射光,不是表面是什么颜色
- 不同材质(布料、木头、金属)有不同的粗糙度和金属度,质感就不一样
- 灯光不是越多越好,分层布光(主光+补光+环境光)就够了
- 可以做几个灯光预设模式,让用户一键切换氛围
- 塑料感通常是因为所有材质都一样光滑,加上灯光太平
SpaceRoom 现在看起来像个真实房间了。下一篇开始做交互:让用户能拖动视角、缩放、平移,真正走进这个 3D 房间里看看。