HarmonyOS 6 上的 3D 天空盒:白天与黑夜,一套场景两种世界
写在前头
最近在看渲染相关的资料,有一句话特别戳我:3D 场景好不好看,一半的功劳在“天”上。模型再精致,背景是一块纯色灰布,看起来就像展柜里的样品;可一旦给它配上完整的天空、合适的光照,同一个模型立刻有了“活在世界里”的感觉。这层包住整个场景的“天”,就是天空盒。
这篇就来做一个带天空盒的 3D 场景:一个模型站在一个“球形世界”中央,白天是蓝天白云、阳光灿烂,一键切到夜晚,变成月亮星星的深蓝夜空;相机可以自动环绕,也可以拖滑块手动环视。为了不被环境拖累,代码用了一张官方示例模型和两张程序生成的等距柱状天空图,不依赖任何外部下载。
先说明一点:3D 渲染属于真机能力,这篇的验证设备是华为 MatePad Edge 真机,模拟器不支持 Component3D 渲染,代码会正常编译,但运行请务必用真机。

天空盒是什么:一句话说清
你站在操场上往任意方向看,眼睛永远不会看到“边界”,因为天空是 360 度包裹着你的。3D 渲染想复刻这件事,最省力的办法就是:把一个巨大的球或立方体罩在相机外面,把天空的图贴在这个罩子内壁上——不管相机转到哪个方向,看到的都是天空。这个“罩子”就是天空盒(Skybox),它的学名叫环境贴图。
天空盒有个隐藏福利:它不只是一个背景。渲染引擎会把天空图当成“来自四面八方的光”,用来给模型补光——漫反射照亮暗面,高光反射映出光泽。这就是常说的图像光照(IBL)。也就是说,天空盒同时干了背景和灯光两份活。
所以你会发现,白天那张图里,模型朝太阳的一侧有暖光;切到夜晚后,整体都冷下来,只有月亮方向的淡淡蓝光。这不是我手动打的光,是天空盒自带的光照信息。
环境的三种背景:先搞清楚能贴什么
HarmonyOS 6 的 @kit.ArkGraphics3D 里,场景背景通过 Environment 资源管理,backgroundType 有四种取值:
- BACKGROUND_NONE:没有背景,纯黑或透明;
- BACKGROUND_IMAGE:普通 2D 图片直接铺满视口;
- BACKGROUND_CUBEMAP:六张立方体面图拼成的天空盒;
- BACKGROUND_EQUIRECTANGULAR:一张 2:1 的全景图,等距柱状投影。
我们日常最容易得到的素材就是 2:1 全景图——网上搜“equirectangular”能找到海量 HDR 天空素材,自己画也很容易。这篇就用 BACKGROUND_EQUIRECTANGULAR,一张 1024×512 的 PNG 就能撑起一个世界。

// 从一张 2:1 全景图创建环境
const env: Environment = await factory.createEnvironment({
name: 'env_day',
uri: $rawfile('sky_day.png')
});
env.backgroundType = EnvironmentBackgroundType.BACKGROUND_EQUIRECTANGULAR;
scene.environment = env; // 挂到场景上,既是背景也是光照
创建环境用的是 SceneResourceFactory 的 createEnvironment,参数就是名字加资源 URI。把它赋给 scene.environment 之后,渲染器会自动完成后面所有工作。
先有个模型:官方示例 glTF
天空盒再漂亮,也得有个主角站在里面。工程里放的是 HarmonyOS 官方示例模型 sample_model.gltf(配套一个 .bin 数据文件),一个几何体模型,自带基础材质,正好适合演示光照变化。加载方式沿用标准流程:

const result: Scene = await Scene.load($rawfile('sample_model.gltf'));
const factory = result.getResourceFactory();
加载之后有两件必做的小事:一是创建相机并置为启用(enabled = true),否则场景是黑的;二是从场景根节点里把模型节点捞出来——遍历子节点,跳过相机,剩下的就是模型,后面旋转它用。
这套“加载 + 找节点 + 旋转”的模式,是 ArkGraphics3D 上手的第一课,学会了,任何 glTF 模型都能这样驱动起来。
白天与夜晚:两张全景图就是两个世界
天空盒最让人上头的玩法,就是换一张图等于换一个世界。我准备了两张 2:1 的全景图:
- 白天版:蓝天渐变 + 右上角太阳 + 几朵模糊的云团,地平线以下淡淡的绿色大地;
- 夜晚版:深蓝渐变夜空 + 月亮 + 一百多颗星星,冷色调到底。
两张图都是我程序生成的:纵向按颜色梯度做渐变,太阳和月亮用手绘圆形叠上柔光,云用白色椭圆加高斯模糊,星星随机撒点。生成脚本写起来不到一百行,完全可控,比到处找素材省事。
切环境就是一个按钮的事:

private switchSky() {
if (!this.scene) return;
this.isNight = !this.isNight;
const env: Environment | null = this.isNight ? this.envNight : this.envDay;
if (env) this.scene.environment = env;
}
切换的瞬间,背景、模型上的反光、整体色调全部联动变化——白天到夜晚的“氛围切换”是即时完成的。这也是天空盒方案对比普通背景图的最大优势:连光照一起换,不是贴一张静态图。
让相机绕着世界转
场景搭好了,得让用户“逛”起来。我做了两种观察方式:
- 自动环绕:相机以模型为中心画圆,边转边保持朝向圆心,像绕着展台转圈看展品;
- 手动滑杆:暂停环绕后,拖动 0~360 的滑块,相机转到指定角度。

private applyCamera(angle: number) {
if (!this.camera) return;
// 位置画圆
this.camera.position = {
x: 4.5 * Math.sin(angle),
y: 0,
z: 4.5 * Math.cos(angle)
};
// 姿态绕 Y 轴转 angle,保证始终看向圆心
this.camera.rotation = {
x: 0,
y: Math.sin(angle / 2),
z: 0,
w: Math.cos(angle / 2)
};
}
姿态用的是四元数:绕 Y 轴转 angle 弧度的四元数是 (0, sin(angle/2), 0, cos(angle/2))。第一次写 3D 的人普遍在这栽跟头——直接用欧拉角(x、y、z 三个旋转角)设置旋转,会出现万向锁和旋转顺序问题;四元数没有这些毛病。记住这个公式,绕任意轴旋转都能套。
模型自己也加了缓慢自转,和相机公转叠在一起,画面就有两层运动,视觉上丰富不少。运动全部用 setInterval 每 16ms 推进一次角度,和帧率基本同步。
完整代码——直接运行
下面是 Index.ets 的完整代码,资源文件(sample_model.gltf、sample_model.bin、sky_day.png、sky_night.png)都放在 rawfile 目录下。工程是 Empty Ability 模板,替换页面文件、把四个资源放进 rawfile 即可。
import { Scene, Node, Camera, Environment, EnvironmentBackgroundType, NodeType } from '@kit.ArkGraphics3D';
@Entry
@Component
struct Index {
private scene: Scene | null = null;
private cubeNode: Node | null = null;
private camera: Camera | null = null;
private envDay: Environment | null = null;
private envNight: Environment | null = null;
private timerId: number = -1;
private modelAngle: number = 0;
private orbitAngle: number = 0;
@State isNight: boolean = false;
@State isOrbiting: boolean = true;
@State viewAngle: number = 0;
@State modelReady: boolean = false;
@State errorMsg: string = '';
aboutToAppear() {
this.initScene();
}
aboutToDisappear() {
this.stopOrbit();
}
private async initScene() {
try {
// 加载 glTF 模型(官方示例)
const result: Scene = await Scene.load($rawfile('sample_model.gltf'));
this.scene = result;
const factory = result.getResourceFactory();
// 相机:默认看向 -Z,放在模型前方
const camera: Camera = await factory.createCamera({ name: 'camera1' });
camera.enabled = true;
camera.position = { x: 0, y: 0, z: 4.5 };
this.camera = camera;
// 两套天空盒:白天 / 夜晚(等距柱状全景图,2:1)
const envDay: Environment = await factory.createEnvironment({ name: 'env_day', uri: $rawfile('sky_day.png') });
envDay.backgroundType = EnvironmentBackgroundType.BACKGROUND_EQUIRECTANGULAR;
this.envDay = envDay;
const envNight: Environment = await factory.createEnvironment({ name: 'env_night', uri: $rawfile('sky_night.png') });
envNight.backgroundType = EnvironmentBackgroundType.BACKGROUND_EQUIRECTANGULAR;
this.envNight = envNight;
result.environment = envDay;
// 找模型节点(跳过相机)
const root = result.root;
if (root) {
for (let i = 0; i < root.children.count(); i++) {
const child = root.children.get(i);
if (child && child.nodeType !== NodeType.CAMERA) {
this.cubeNode = child;
break;
}
}
}
this.modelReady = true;
this.startOrbit();
} catch (err) {
const e = err as Error;
this.errorMsg = e && e.message ? e.message : JSON.stringify(err);
console.error('场景初始化失败: ' + this.errorMsg);
}
}
// 模型自转 + 相机自动环绕
private startOrbit() {
if (this.timerId !== -1) return;
this.timerId = setInterval(() => {
this.modelAngle += 0.015;
if (this.modelAngle >= 2 * Math.PI) this.modelAngle -= 2 * Math.PI;
if (this.cubeNode) {
this.cubeNode.rotation = {
x: 0, y: Math.sin(this.modelAngle / 2), z: 0, w: Math.cos(this.modelAngle / 2)
};
}
if (this.isOrbiting) {
this.orbitAngle += 0.008;
if (this.orbitAngle >= 2 * Math.PI) this.orbitAngle -= 2 * Math.PI;
this.applyCamera(this.orbitAngle);
}
}, 16);
}
private applyCamera(angle: number) {
if (!this.camera) return;
// 相机绕 Y 轴公转,位置画圆,朝向始终对准模型
this.camera.position = {
x: 4.5 * Math.sin(angle), y: 0, z: 4.5 * Math.cos(angle)
};
this.camera.rotation = {
x: 0, y: Math.sin(angle / 2), z: 0, w: Math.cos(angle / 2)
};
this.viewAngle = Math.round(angle * 180 / Math.PI);
}
private stopOrbit() {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
private toggleOrbit() {
this.isOrbiting = !this.isOrbiting;
}
private switchSky() {
if (!this.scene) return;
this.isNight = !this.isNight;
const env: Environment | null = this.isNight ? this.envNight : this.envDay;
if (env) this.scene.environment = env;
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
// 3D 场景占满主体
if (this.modelReady && this.scene) {
Component3D({ scene: this.scene, modelType: ModelType.SURFACE })
.width('100%')
.height('100%')
} else {
this.loadingView()
}
}
.width('100%')
.height('100%')
// 底部控制条
Column({ space: 12 }) {
Row() {
Text(this.isNight ? '夜晚天空盒' : '白天天空盒')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Blank()
Button(this.isNight ? '切回白天' : '切到夜晚')
.height(32)
.fontSize(13)
.backgroundColor(this.isNight ? '#FFB300' : '#3949AB')
.onClick(() => { this.switchSky(); })
}
.width('100%')
Row() {
Text('环绕视角')
.fontSize(13)
.fontColor('rgba(255,255,255,0.85)')
Blank()
Text(this.isOrbiting ? '自动环绕中' : '手动控制')
.fontSize(13)
.fontColor('#80D8FF')
}
.width('100%')
Row() {
Button(this.isOrbiting ? '暂停环绕' : '自动环绕')
.height(30)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#455A64')
.onClick(() => { this.toggleOrbit(); })
Blank()
Slider({ value: this.viewAngle, min: 0, max: 360, style: SliderStyle.OutSet })
.layoutWeight(1)
.enabled(!this.isOrbiting)
.onChange((value: number) => {
this.orbitAngle = value * Math.PI / 180;
this.applyCamera(this.orbitAngle);
})
}
.width('100%')
.alignItems(VerticalAlign.Center)
Text('提示:3D 场景与天空盒属于渲染能力,请用真机运行体验;滑块可手动环视四周,按钮一键切换昼夜。')
.fontSize(12)
.fontColor('rgba(255,255,255,0.7)')
.lineHeight(18)
}
.width('100%')
.padding(16)
.backgroundColor('rgba(0,0,0,0.55)')
.borderRadius(16)
.margin(12)
}
.width('100%')
.height('100%')
.backgroundColor('#0B1220')
}
@Builder
loadingView() {
Column() {
if (this.errorMsg !== '') {
Text('场景初始化失败:' + this.errorMsg)
.fontSize(13)
.fontColor('#FF6B6B')
.textAlign(TextAlign.Center)
.padding(16)
} else {
Text('场景加载中...')
.fontSize(16)
.fontColor('#8899AA')
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

真机上看什么
在 MatePad Edge 上跑起来:模型站在球形世界里,头顶是铺开的天空。白天模式下,蓝天从头顶的深蓝过渡到地平线的浅青,太阳一侧的模型面被暖光照亮,云团在“天际”缓缓贴着;切到夜晚,整片天暗下来,月亮泛着冷光,星星在移动视角时呈现近大远小的视差——因为天空盒是贴在一个大球上的,相机一转,整个星空都在转。
最值得玩的还是视角:点“暂停环绕”,拖滑块到 0°、90°、180°、270° 分别停一下,你会看到模型在太阳光、侧光、背光之间的亮度变化,这正是天空盒同时充当光照的证据。切回自动环绕,让相机自己转,模型自转叠着公转,一个“活”的世界就出来了。
如果模型看起来偏暗,多半是环境间接光强度的问题,可以在 Environment 上调节 indirectDiffuseFactor(漫反射间接光)和 indirectSpecularFactor(高光间接光),把两个因子调高一点,整体就会亮起来。这个参数也是控制“天空盒氛围浓度”的旋钮。
收尾
这篇从“天空盒是什么”讲到双环境切换,再到相机环绕,把 ArkGraphics3D 里最出效果的一环串了起来。回头看,天空盒方案真正厉害的地方不是“贴背景”,而是“给光”——一张全景图同时决定背景和光照,换一张图,整个世界连同模型的明暗一起变。
留两个课后作业:第一,把天空图换成你自己拍的 2:1 全景照片,看看真实世界的天空在模型上的反光效果;第二,试着在 Environment 上加第三个环境(比如黄昏),做一个“时间轴”切换器,从清晨到深夜一口气滑过去。天空盒这扇门推开之后,3D 场景的质感会直接上一个台阶。
参考资料
更多推荐


所有评论(0)