给 3D 物体换皮肤:材质、图片、着色器这些资源怎么用

你有没有在游戏里见过那种金属质感的盔甲、布料质感的衣服、或者玻璃质感的酒杯?这些不同的"皮肤"效果,在 3D 开发里叫做"材质(Material)"。材质决定了一个物体看起来是什么质感——是光滑的还是粗糙的,是金属的还是塑料的,是会发光的还是暗淡的。

这篇文章就来聊聊 ArkGraphics 3D 里的资源系统,包括材质、图片、着色器、采样器这些核心资源类型,以及怎么创建和使用它们。

ArkGraphics 3D 资源系统架构

下面是 ArkGraphics 3D 资源系统的整体架构:

SceneResource 基类

材质 Material

着色器 Shader

图片 Image

采样器 Sampler

网格 Mesh

环境 Environment

动画 Animation

SHADER 着色器材质

METALLIC_ROUGHNESS PBR材质

UNLIT 无光照材质

OCCLUSION 遮挡材质

CustomGeometry 自定义

CubeGeometry 立方体

PlaneGeometry 平面

SphereGeometry 球体

SceneResourceFactory

资源系统的整体结构

在 ArkGraphics 3D 里,所有资源都继承自 SceneResource 这个基类。它有几个通用属性:

  • name:资源名称,你自己起的,用来标识这个资源
  • resourceType:资源类型,只读,是一个枚举值
  • uri:资源文件路径,只读

资源类型用 SceneResourceType 枚举来区分:NODE(节点)、ENVIRONMENT(环境)、MATERIAL(材质)、MESH(网格)、ANIMATION(动画)、SHADER(着色器)、IMAGE(图片)、MESH_RESOURCE(网格资源)、EFFECT(特效)。

所有资源都可以通过 destroy() 方法来释放。记住,一旦释放就不能再用了。

材质:物体的"皮肤"

材质是最核心的资源类型之一。ArkGraphics 3D 支持四种材质类型,用 MaterialType 枚举来区分:

  • SHADER(1):着色器材质,完全由自定义着色器控制渲染效果
  • METALLIC_ROUGHNESS(2):金属-粗糙度材质,基于物理渲染(PBR)模型,最常用的材质类型
  • UNLIT(3):不受光照影响的材质,适合做 UI 元素或特效
  • OCCLUSION(4):遮挡材质,能遮挡其他物体但不遮挡环境

创建材质

通过 SceneResourceFactorycreateMaterial 方法来创建材质:

import { MaterialType, Material, SceneResourceParameters, SceneResourceFactory, Scene } from '@kit.ArkGraphics3D';

function createMaterialPromise() : Promise<Material> {
  return new Promise((resolve, reject) => {
    // 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
    let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
    scene.then(async (result: Scene) => {
      let sceneFactory: SceneResourceFactory = result.getResourceFactory();
      let sceneMaterialParameter: SceneResourceParameters = { name: "material" };
      // 创建材质
      let material: Material = await sceneFactory.createMaterial(sceneMaterialParameter, MaterialType.SHADER);
      resolve(material);
    }).catch((error: Error) => {
      console.error('Scene load failed:', error);
      reject(error);
    });
  });
}

这里创建的是一个着色器材质(MaterialType.SHADER)。SceneResourceParameters 只需要一个 name,用来标识这个材质资源。

Material 的通用属性

不管什么类型的材质,都有一些通用属性:

  • materialType:材质类型,只读
  • shadowReceiver:是否接收阴影,默认 false
  • cullMode:剔除模式,控制是否剔除正面或背面的面片。默认是 BACK(剔除背面),这在大多数情况下是正确的——你从外面看一个立方体,不需要渲染它内部的面
  • blend:透明效果,{ enabled: true } 开启透明
  • alphaCutoff:透明通道阈值,低于这个阈值的像素不渲染
  • renderSort:渲染排序,控制不同物体的绘制先后顺序
  • polygonMode:多边形绘制模式,FILL(填充)、LINE(线框)、POINT(只画顶点)

PBR 金属-粗糙度材质

这是最常用的材质类型,能模拟金属、塑料、布料等各种真实质感。它继承自 Material,有以下属性:

  • baseColor:基础颜色,决定物体在没有光照时的颜色
  • normal:法线贴图,让物体表面看起来有凹凸细节,但实际几何形状不变
  • material:金属材质参数,包含粗糙度、金属度、反射度
  • ambientOcclusion:环境光遮蔽贴图,模拟凹陷处的阴影
  • emissive:自发光颜色
  • clearCoat:透明图层,类似车漆效果
  • clearCoatRoughness:透明图层粗糙度
  • clearCoatNormal:透明图层法线贴图
  • sheen:微纤维光泽,适合布料材质
  • specular:非金属的高光反射

每个属性都是 MaterialProperty 类型,包含三个字段:

  • image:纹理贴图(Image 类型或 null)
  • factor:属性因子(Vec4 类型),当没有纹理贴图时,用这个值作为默认属性
  • sampler:采样器,控制纹理的采样方式

简单说,factor 是"纯色模式",image 是"贴图模式"。比如你要做一个纯红色的塑料球,只需要把 baseColor 的 factor 设为红色就行;如果你要做一个有木纹的桌面,就需要给 baseColor 的 image 指定一张木纹贴图。

着色器材质

着色器材质给你最大的自由度——你可以完全控制渲染逻辑。它只有一个属性:

  • colorShader:着色器对象

Unlit 材质和遮挡材质

UnlitMaterial(API 23+)不受光照影响,只有一个 baseColor 属性。适合做 UI 上的 3D 元素,或者你想让某个物体始终保持恒定亮度的场景。

OcclusionMaterial(API 23+)是一个特殊材质,它能遮挡场景中的其他物体,但不会遮挡环境背景。典型的用途是做 AR 场景中的"遮挡体"——比如虚拟家具放在真实地面上,地面需要用遮挡材质来挡住虚拟家具的底部。

着色器:渲染的灵魂

着色器(Shader)是一段运行在 GPU 上的程序,它决定了物体最终呈现的样子。在 ArkGraphics 3D 里,着色器通过 .shader 文件来定义。

创建着色器

import { Shader, SceneResourceParameters, SceneResourceFactory, Scene } from '@kit.ArkGraphics3D';

function createShaderPromise(): Promise<Shader> {
  return new Promise((resolve, reject) => {
    // 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
    let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
    scene.then(async (result: Scene) => {
      let sceneFactory: SceneResourceFactory = result.getResourceFactory();

      // 创建shader资源(通过SceneResourceParameters配置),路径和文件名可根据项目实际资源自定义
      let sceneResourceParameter: SceneResourceParameters = { name: "shaderResource",
        uri: $rawfile("shaders/custom_shader/custom_material_sample.shader") };
      let shader: Shader = await sceneFactory.createShader(sceneResourceParameter);
      resolve(shader);
    }).catch((error: Error) => {
      console.error('Scene load failed:', error);
      reject(error);
    });
  });
}

这次 SceneResourceParameters 需要指定 uri,指向你的 .shader 文件。

设置着色器输入

着色器需要外部传入一些参数(uniform),比如时间、颜色、纹理等。你可以通过 inputs 属性或 setShaderInputs 方法来设置:

import { Image, MaterialType, Scene, SceneResourceFactory, Shader, ShaderMaterial } from '@kit.ArkGraphics3D';

function setinputs(): void {
  // 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
  let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
  scene.then(async (result: Scene) => {
    if (result) {
      let rf : SceneResourceFactory | null = await result.getResourceFactory();
      if (!rf) {
        return;
      }
      // 创建材质和shader
      let material: ShaderMaterial | null = await rf.createMaterial({name: "CustomMaterial"}, MaterialType.SHADER);
      let shader : Shader | null = await rf.createShader(
        {name: "CustomShader", uri: $rawfile("shaders/custom_shader/custom_material_sample.shader")});
      if (!material || !shader) {
        return;
      }
      // 加载纹理资源
      let image : Image | null = await rf.createImage({name: "envImg", uri: $rawfile("custom_image.jpg")});
      if (!image) {
        return;
      }
      // 绑定shader到纹理上
      material.colorShader = shader;
      // 设置shader输入
      material.colorShader.setShaderInputs({
        "uTime": 1.0,
        "uVelocity": {x: 1.0, y: 1.0, z:-1.0, w:-1.0},
        "uTexture": image
      })
    }
  });
}

这段代码展示了一个完整的流程:创建着色器材质、创建着色器、加载图片纹理、把着色器绑定到材质上、设置着色器输入。

setShaderInputs 接受一个键值对对象,值可以是 numberVec2Vec3Vec4Image。注意 setShaderInputs 方法(API 23+)比直接设置 inputs 属性性能更好,推荐使用。

图片资源

图片(Image)在 3D 场景中用途广泛——贴图、环境贴图、UI 纹理等。

import { Image, SceneResourceParameters, Scene, RenderContext, RenderResourceFactory } from '@kit.ArkGraphics3D';

function createImageResource(): Promise<Image> {
  const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
  if (!renderContext) {
    return Promise.reject(new Error("RenderContext is null"));
  }
  const renderResourceFactory: RenderResourceFactory = renderContext.getRenderResourceFactory();
  // 加载图片资源,路径和文件名可根据项目实际资源自定义
  let imageParams: SceneResourceParameters = {
    name: "sampleImage",
    uri: $rawfile("image/Cube_BaseColor.png")
  };
  return renderResourceFactory.createImage(imageParams);
}

Image 有两个只读属性:width(宽度)和 height(高度),单位都是像素。

注意这里用的是 RenderResourceFactory 而不是 SceneResourceFactory。两者都可以创建图片资源,区别是 RenderResourceFactory 创建的资源可以在多个场景之间共享。

采样器:控制纹理的采样方式

采样器(Sampler)决定了纹理在被放大或缩小时怎么处理像素。

import { SceneResourceParameters, Scene, RenderContext, RenderResourceFactory, Sampler } from '@kit.ArkGraphics3D';

function createSamplerResource(): Promise<Sampler> {
  const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
  if (!renderContext) {
    return Promise.reject(new Error("RenderContext is null"));
  }
  const renderResourceFactory: RenderResourceFactory = renderContext.getRenderResourceFactory();
  // 加载图片资源,路径和文件名可根据项目实际资源自定义
  let samplerParams: SceneResourceParameters = {
    name: "sampler1",
    uri: $rawfile("image/Cube_BaseColor.png")
  };
  return renderResourceFactory.createSampler(samplerParams);
}

Sampler 的属性包括:

  • magFilter:放大过滤模式,纹理被放大时怎么采样
  • minFilter:缩小过滤模式,纹理被缩小时怎么采样
  • mipMapMode:mipmap 过滤模式,多级纹理之间的采样方式
  • addressModeU:U 方向(水平)的寻址模式
  • addressModeV:V 方向(垂直)的寻址模式

过滤模式有两种:

  • NEAREST(0):最近邻插值,速度快但可能有锯齿
  • LINEAR(1):线性插值,效果平滑但性能略低

寻址模式有三种:

  • REPEAT(0):重复平铺,坐标超出 [0,1] 时纹理会重复
  • MIRRORED_REPEAT(1):镜像重复
  • CLAMP_TO_EDGE(2):边缘拉伸

默认值都是 LINEAR 和 REPEAT,对大多数场景来说够用了。

网格资源

网格(MeshResource)定义了物体的几何形状。你可以通过代码来创建自定义网格:

import { SceneResourceParameters, Scene, CustomGeometry, PrimitiveTopology, RenderContext, RenderResourceFactory,
  MeshResource }  from '@kit.ArkGraphics3D';

function createMeshResource(): Promise<MeshResource> {
  const renderContext: RenderContext | null = Scene.getDefaultRenderContext();
  if (!renderContext) {
    return Promise.reject(new Error("RenderContext is null"));
  }
  const renderResourceFactory: RenderResourceFactory = renderContext.getRenderResourceFactory();
  const geometry = new CustomGeometry();
  geometry.vertices = [
    { x: 0, y: 0, z: 0 },
    { x: 1, y: 0, z: 0 },
    { x: 1, y: 1, z: 0 },
    { x: 0, y: 1, z: 0 },
    { x: 0, y: 0, z: 1 },
    { x: 1, y: 0, z: 1 },
    { x: 1, y: 1, z: 1 },
    { x: 0, y: 1, z: 1 }
  ];
  geometry.indices = [
    0, 1, 2, 2, 3, 0,     // front
    4, 5, 6, 6, 7, 4,     // back
    0, 4, 5, 5, 1, 0,     // bottom
    1, 5, 6, 6, 2, 1,     // right
    3, 2, 6, 6, 7, 3,     // top
    3, 7, 4, 4, 0, 3      // left
  ];
  geometry.topology = PrimitiveTopology.TRIANGLE_LIST;
  geometry.normals = [
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 },
    { x: 0, y: 0, z: 1 }
  ];
  geometry.uvs = [
    { x: 0, y: 0 },
    { x: 1, y: 0 },
    { x: 1, y: 1 },
    { x: 0, y: 1 },
    { x: 0, y: 0 },
    { x: 1, y: 0 },
    { x: 1, y: 1 },
    { x: 0, y: 1 }
  ];
  geometry.colors = [
    { r: 1, g: 0, b: 0, a: 1 },
    { r: 0, g: 1, b: 0, a: 1 },
    { r: 0, g: 0, b: 1, a: 1 },
    { r: 1, g: 1, b: 0, a: 1 },
    { r: 1, g: 0, b: 1, a: 1 },
    { r: 0, g: 1, b: 1, a: 1 },
    { r: 1, g: 1, b: 1, a: 1 },
    { r: 0, g: 0, b: 0, a: 1 }
  ];
  // 加载图片资源,路径和文件名可根据项目实际资源自定义
  let sceneResourceParameter: SceneResourceParameters = {
    name: "cubeMesh",
    uri: $rawfile("image/Cube_BaseColor.png")
  };
  return renderResourceFactory.createMesh(sceneResourceParameter, geometry);
}

这段代码用 CustomGeometry 手动定义了一个立方体的几何数据:

  • vertices:8 个顶点的坐标
  • indices:顶点索引,每 3 个一组构成一个三角形(一个立方体 6 个面,每个面 2 个三角形,共 12 个三角形,36 个索引)
  • topology:图元拓扑,TRIANGLE_LIST 表示每 3 个顶点构成一个独立的三角形
  • normals:法线向量,用于光照计算
  • uvs:UV 坐标,用于纹理映射
  • colors:顶点颜色

除了 CustomGeometry,ArkGraphics 3D 还提供了几种内置几何体:

  • CubeGeometry:立方体,只需指定 size(宽高深)
  • PlaneGeometry:平面,只需指定 size(宽高)
  • SphereGeometry:球体,需要指定 radius(半径)和 segmentCount(分段数)
  • CylinderGeometry(API 23+):圆柱体,需要指定 radiusheightsegmentCount

用内置几何体比手动定义顶点简单得多:

import { SceneResourceFactory, Scene, Geometry, CubeGeometry } from '@kit.ArkGraphics3D';

function createGeometryPromise() : Promise<Geometry> {
  return new Promise((resolve, reject) => {
    let scene: Promise<Scene> = Scene.load();
    scene.then(async (result: Scene | undefined) => {
      if (!result) {
        return;
      }
      let sceneFactory: SceneResourceFactory = result.getResourceFactory();
      // 创建立方体几何数据
      let cubeGeom = new CubeGeometry();
      cubeGeom.size = { x: 1, y: 1, z: 1 };
      // 根据立方体几何数据创建网格资源
      let meshRes = await sceneFactory.createMesh({ name: "MeshName" }, cubeGeom);
      console.info("TEST createGeometryPromise");
      // 根据场景节点参数和网格资源创建几何对象
      let geometry: Geometry = await sceneFactory.createGeometry({ name: "GeometryName" }, meshRes);
      resolve(geometry);
    }).catch((error: Error) => {
      console.error('Scene load failed:', error);
      reject(error);
    });
  });
}

这里先创建一个 1x1x1 的立方体几何定义,然后用它创建网格资源,最后创建几何节点。几何节点是可以直接放到场景树里的。

环境:场景的光照和背景

环境(Environment)控制场景的整体光照和背景。

import { Environment, SceneResourceParameters, SceneResourceFactory, Scene } from '@kit.ArkGraphics3D';

function createEnvironmentPromise(): Promise<Environment> {
  return new Promise((resolve, reject) => {
    // 加载场景资源,支持.gltf和.glb格式,路径和文件名可根据项目实际资源自定义
    let scene: Promise<Scene> = Scene.load($rawfile("gltf/CubeWithFloor/glTF/AnimatedCube.glb"));
    scene.then(async (result: Scene) => {
      let sceneFactory: SceneResourceFactory = result.getResourceFactory();
      // 加载环境贴图资源,路径和文件名可根据项目实际资源自定义
      let sceneEnvironmentParameter: SceneResourceParameters = { name: "env", uri: $rawfile("KTX/quarry_02_2k_radiance.ktx") };
      // 创建Environment
      let env: Environment = await sceneFactory.createEnvironment(sceneEnvironmentParameter);
      resolve(env);
    }).catch((error: Error) => {
      console.error('Scene load failed:', error);
      reject(error);
    });
  });
}

Environment 的属性包括:

  • backgroundType:背景类型,可以是无背景、图片背景、立方体贴图背景或等距柱状投影背景
  • indirectDiffuseFactor:间接散射系数
  • indirectSpecularFactor:间接反射系数
  • environmentMapFactor:环境贴图系数
  • environmentImage:环境图片
  • radianceImage:辐射图片
  • irradianceCoefficients:辐射系数
  • environmentRotation(API 23+):环境光旋转,接收归一化四元数

环境贴图通常用 .ktx 格式的 HDR 图片,它包含了场景周围的光照信息,PBR 材质会用这些信息来做反射和全局光照计算。

动画资源

如果 glTF 模型里带有动画数据,加载后会自动解析到 Scene.animations 数组里。每个 Animation 对象有以下属性:

  • enabled:是否启用
  • speed:播放速度,默认 1.0,负值表示反向播放
  • duration:动画时长(秒)
  • running:是否正在播放
  • progress:播放进度 [0, 1]

控制动画的方法有:start()stop()pause()restart()seek(position)finish()。还有两个回调:onStartedonFinished,分别在动画开始和结束时触发。

形变器(Morpher)

形变器(API 20+)用于控制模型的形变效果,比如让人脸做出不同的表情。它有一个 targets 属性,是一个键值对,键是形变目标的名称,值是权重(通常在 0 到 1 之间)。

PBR 材质属性配置流程

下面是 PBR 金属-粗糙度材质的属性配置流程:

纯色

贴图

创建 PBR 材质

配置基础属性

baseColor 基础颜色

normal 法线贴图

material 金属参数

纯色还是贴图?

设置 factor 值

设置 image 纹理

配置高级属性

ambientOcclusion 环境遮蔽

emissive 自发光

clearCoat 透明图层

sheen 微纤维光泽

绑定到几何节点

渲染输出

小结

ArkGraphics 3D 的资源系统层次清晰:

  • 材质决定物体的外观质感,PBR 材质最常用,着色器材质最灵活
  • 着色器是 GPU 上运行的程序,控制渲染的每一个像素
  • 图片是纹理的载体,可以给材质贴上各种图案
  • 采样器控制纹理的采样精度和重复方式
  • 网格定义物体的几何形状,可以用内置几何体或自定义顶点
  • 环境控制场景的整体光照和背景

这些资源通过 SceneResourceFactoryRenderResourceFactory 来创建,通过 SceneResourceParameters 来配置参数。

下一篇文章我们来聊聊 3D 开发中最基础的数据类型——向量和四元数。搞懂这些,你才能真正理解 3D 空间中的位置、方向和旋转。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐