在现代三维地理信息系统(GIS)和城市建模应用中,3D Tiles 作为一种高效的传输和渲染大规模三维城市数据的格式,已成为行业标准之一。随着数据规模的不断增长,如何灵活地调整三维场景的外观和风格成为提升用户体验的重要课题。为此,Custom Shader(自定义着色器) 提供了一种强大的工具,允许开发者根据实际需求,定制和优化场景的视觉效果。
本文将介绍如何使用 Custom Shader 技术,基于 3D Tiles 格式调整三维模型的渲染风格。通过这一技术,开发者不仅可以实现更加精细的视觉控制,还能有效地提升渲染性能和场景表现力。文章将从基本的着色器概念出发,逐步引导读者了解如何在 3D Tiles 中应用自定义着色器,并结合实际案例展示其在不同场景下的应用方式。
TilesBuilder : TilesBuilder提供一个高效、兼容、优化的数据转换工具,一站式完成数据转换、数据发布、数据预览操作。
CustomShader
是一个用于在 Cesium 中为 3D Tiles 或模型添加自定义着色器的功能。你可以利用它来调整模型或 tileset 的渲染风格,修改材质、光照模型等。以下是如何使用 CustomShader
来实现风格调整的步骤:
1. 创建 CustomShader 实例
首先,你需要定义一个 CustomShader
,并设置其中的 uniform 和 varyings。
js
const customShader = new Cesium.CustomShader({
uniforms: {
u_time: {
value: 0, // 初始值
type: Cesium.UniformType.FLOAT
},
u_externalTexture: {
value: new Cesium.TextureUniform({
url: "http://example.com/image.png"
}),
type: Cesium.UniformType.SAMPLER_2D
}
},
varyings: {
v_customTexCoords: Cesium.VaryingType.VEC2
},
mode: Cesium.CustomShaderMode.MODIFY_MATERIAL, // 可以选择修改材质或替换材质
lightingModel: Cesium.LightingModel.PBR, // 使用 PBR 光照模型
translucencyMode: Cesium.CustomShaderTranslucencyMode.TRANSLUCENT, // 透明效果
vertexShaderText: `
void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) {
// 在这里添加顶点着色器代码
}
`,
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
// 在这里设置材质的颜色
material.diffuse = vec3(1.0, 0.0, 0.0); // 设置为红色
material.alpha = 0.5; // 设置透明度
}
`
});
2. 将 CustomShader 应用到 3D Tiles 或模型
你可以将 CustomShader
应用到 3D Tiles
或单独的 Model
。具体实现如下:
应用到整个 3D Tiles 集合
js
const tileset = await Cesium.Cesium3DTileset.fromUrl(
"http://example.com/tileset.json", {
customShader: customShader
}
);
viewer.scene.primitives.add(tileset);
应用到单个模型
js
const model = await Cesium.Model.fromGltfAsync({
url: "http://example.com/model.gltf",
customShader: customShader
});
3. 使用 Uniforms 和 Varyings
Uniforms 用于在整个着色器中共享数据,例如时间、纹理等。而 Varyings 用于在顶点着色器和片元着色器之间传递数据。
使用 Varying 示例:
在顶点着色器中设置 v_selectedColor
,并在片元着色器中使用它。
js
const customShader = new Cesium.CustomShader({
varyings: {
v_selectedColor: Cesium.VaryingType.VEC4
},
vertexShaderText: `
void vertexMain(VertexInput vsInput, inout czm_modelVertexOutput vsOutput) {
// 设置颜色,根据顶点的 X 坐标混合颜色
float positiveX = step(0.0, vsOutput.positionMC.x);
v_selectedColor = mix(vsInput.attributes.color_0, vsInput.attributes.color_1, positiveX);
}
`,
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
material.diffuse = v_selectedColor.rgb; // 使用顶点传递过来的颜色
}
`
});
4. 使用自定义光照模型
CustomShader
允许你选择不同的光照模型。例如,选择 PBR(物理基础渲染)模型:
js
lightingModel: Cesium.LightingModel.PBR
或者,你也可以选择 UNLIT 渲染,这样模型就没有光照效果。
5. 设置透明度
你还可以设置透明度来实现不同的视觉效果:
js
translucencyMode: Cesium.CustomShaderTranslucencyMode.TRANSLUCENT
6.Attributes
结构体
Attributes
结构体是根据自定义着色器中使用的变量和模型中可用的属性动态生成的。通过这种方式,着色器可以自动绑定模型中的属性,并使用它们来渲染效果。如果自定义着色器中引用了某个属性(例如 fsInput.attributes.texCoord_0
),Cesium 会自动生成必要的代码来从模型中的 TEXCOORD_0
属性提供数据。
如果某个原始数据缺失,Cesium 会尽量为该属性推断一个默认值,以便着色器仍然能够编译。如果无法推断,相关的顶点或片元着色器部分会被禁用。
常见属性字段及其说明
模型中的属性 | 着色器中的变量名 | 类型 | 顶点着色器中可用 | 片元着色器中可用 | 说明 |
---|---|---|---|---|---|
POSITION |
positionMC |
vec3 |
是 | 是 | 模型坐标系中的位置 |
POSITION |
positionWC |
vec3 |
否 | 是 | 世界坐标系中的位置 (WGS84 ECEF (x, y, z) ) 精度较低 |
POSITION |
positionEC |
vec3 |
否 | 是 | 眼坐标系中的位置 |
NORMAL |
normalMC |
vec3 |
是 | 否 | 模型坐标系中的单位法线向量 |
NORMAL |
normalEC |
vec3 |
否 | 是 | 眼坐标系中的单位法线向量 |
TANGENT |
tangentMC |
vec3 |
是 | 否 | 模型坐标系中的单位切线向量 |
TANGENT |
tangentEC |
vec3 |
否 | 是 | 眼坐标系中的单位切线向量 |
NORMAL & TANGENT |
bitangentMC |
vec3 |
是 | 否 | 模型坐标系中的单位副法线向量,仅在法线和切线都可用时提供 |
NORMAL & TANGENT |
bitangentEC |
vec3 |
否 | 是 | 眼坐标系中的单位副法线向量,仅在法线和切线都可用时提供 |
TEXCOORD_N |
texCoord_N |
vec2 |
是 | 是 | 第 N 组纹理坐标 |
COLOR_N |
color_N |
vec4 |
是 | 是 | 第 N 组顶点颜色,通常是 vec4 类型,如果没有提供 alpha 值,默认值为 1 |
JOINTS_N |
joints_N |
ivec4 |
是 | 是 | 第 N 组关节索引 |
WEIGHTS_N |
weights_N |
vec4 |
是 | 是 | 第 N 组权重 |
自定义属性也可以被使用,它们在着色器中会被重命名为小写字母和下划线。例如,模型中有一个属性 _SURFACE_TEMPERATURE
,在着色器中会变成 fsInput.attributes.surface_temperature
。
FeatureIds
结构体
FeatureIds
结构体用于收集所有的特征 ID,不论这些 ID 是来自于属性、纹理还是 varying。特征 ID 在 GLSL 中表示为 int
类型,但在 WebGL 1 中存在一些限制:
- 超过
2^24
时,精度可能会丢失,因为 WebGL 1 使用highp int
作为浮动点类型。 - 理想情况下,类型应该为
uint
,但是直到 WebGL 2 才支持。
3D Tiles 1.0 批次 ID
在 3D Tiles 1.0 中,特征 ID 是通过 BATCH_ID
或其旧版 _BATCHID
来识别的。这些批次 ID 被重命名为特征 ID,通常具有索引 0:
- 顶点着色器:
vsInput.featureIds.featureId_0
- 片元着色器:
fsInput.featureIds.featureId_0
EXT_mesh_features
/EXT_instance_features
特征 ID
当 glTF 扩展 EXT_mesh_features
或 EXT_instance_features
被使用时,特征 ID 出现在以下两个位置:
-
glTF 原语中的
featureIds
数组 :featureIds
数组可能包含特征 ID 属性、隐式特征 ID 属性和特征 ID 纹理。这些特征 ID 会出现在自定义着色器中,例如vsInput.featureIds.featureId_N
(顶点着色器)或fsInput.featureIds.featureId_N
(片元着色器)。 -
具有
EXT_mesh_gpu_instancing
和EXT_instance_features
的 glTF 节点 :这些节点可以定义特征 ID,这些 ID 出现在自定义着色器中,通常标记为instanceFeatureId_N
。
特征 ID 纹理
特征 ID 纹理仅在片元着色器中支持,因此在片元着色器中使用特征 ID 纹理时,可以访问:
glsl
fsInput.featureIds.featureId_0
7.例子
假设我们有一个 glTF 模型,其特征 ID 配置如下:
json
"nodes": [
{
"mesh": 0,
"extensions": {
"EXT_mesh_gpu_instancing": {
"attributes": {
"TRANSLATION": 3,
"_FEATURE_ID_0": 4
}
},
"EXT_instance_features": {
"featureIds": [
{
"label": "perInstance",
"propertyTable": 0
},
{
"propertyTable": 1,
"attribute": 0
}
]
}
}
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"POSITION": 0,
"_FEATURE_ID_0": 1,
"_FEATURE_ID_1": 2
},
"extensions": {
"EXT_mesh_features": {
"featureIds": [
{
"label": "texture",
"propertyTable": 2,
"index": 0,
"texCoord": 0,
"channel": 0
},
{
"label": "perVertex",
"propertyTable": 3
},
{
"propertyTable": 4,
"attribute": 0
},
{
"propertyTable": 5,
"attribute": 1
}
]
}
}
}
]
}
]
在此示例中,特征 ID 会通过不同的方式出现在自定义着色器中。例如,你可以在片元着色器中使用 fsInput.featureIds.featureId_0
来访问默认特征 ID,或使用 fsInput.featureIds.texture
来访问纹理特征 ID。
这种方式可以帮助你在 Cesium 中为每个原语或实例应用不同的风格、效果或属性。
TilesBuilder : TilesBuilder提供一个高效、兼容、优化的数据转换工具,一站式完成数据转换、数据发布、数据预览操作。
例子 1:使用纹理坐标和顶点颜色进行渲染
假设我们有一个自定义着色器,其中使用了纹理坐标和顶点颜色来为模型添加基础的纹理和颜色效果。该着色器还会使用模型中的法线来进行简单的光照计算。
顶点着色器 (custom_vertex_shader.glsl
)
glsl
// 传入的属性
in vec3 positionMC; // 模型坐标系中的位置
in vec3 normalMC; // 模型坐标系中的法线
in vec2 texCoord_0; // 第一组纹理坐标
in vec4 color_0; // 第一组顶点颜色
// 输出到片元着色器的变量
out vec2 fragTexCoord; // 传递纹理坐标
out vec4 fragColor; // 传递顶点颜色
void main() {
// 计算模型坐标系中的变换(这里使用一个假定的模型变换矩阵)
vec4 worldPosition = modelMatrix * vec4(positionMC, 1.0);
// 将变换后的坐标传递给光照计算和片元着色器
fragTexCoord = texCoord_0; // 纹理坐标传递给片元着色器
fragColor = color_0; // 顶点颜色传递给片元着色器
// 计算世界坐标系中的位置
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
片元着色器 (custom_fragment_shader.glsl
)
glsl
// 传入的来自顶点着色器的变量
in vec2 fragTexCoord; // 纹理坐标
in vec4 fragColor; // 顶点颜色
// 输出到渲染管线的最终颜色
out vec4 finalColor;
uniform sampler2D textureSampler; // 纹理采样器
void main() {
// 获取纹理颜色
vec4 texColor = texture(textureSampler, fragTexCoord);
// 将纹理颜色与顶点颜色相乘,得到最终的颜色
finalColor = texColor * fragColor;
}
在这个例子中,顶点着色器传递了纹理坐标和顶点颜色到片元着色器,片元着色器将纹理和顶点颜色混合在一起,以实现最终的显示效果。
例子 2:使用法线和切线进行基本光照
假设我们有一个自定义着色器,结合法线和切线向量来进行基础的光照计算。这个例子会演示如何在片元着色器中进行简单的 Lambertian 光照计算。
顶点着色器 (custom_vertex_shader.glsl
)
glsl
// 传入的属性
in vec3 positionMC; // 模型坐标系中的位置
in vec3 normalMC; // 模型坐标系中的法线
in vec3 tangentMC; // 模型坐标系中的切线
// 输出到片元着色器的变量
out vec3 fragNormal; // 传递法线到片元着色器
out vec3 fragTangent; // 传递切线到片元着色器
void main() {
// 计算模型坐标系中的变换(这里使用一个假定的模型变换矩阵)
vec4 worldPosition = modelMatrix * vec4(positionMC, 1.0);
// 将法线和切线变换到世界坐标系
fragNormal = normalize(mat3(modelMatrix) * normalMC);
fragTangent = normalize(mat3(modelMatrix) * tangentMC);
// 计算世界坐标系中的位置
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
片元着色器 (custom_fragment_shader.glsl
)
glsl
// 传入的来自顶点着色器的变量
in vec3 fragNormal; // 法线
in vec3 fragTangent; // 切线
// 输出到渲染管线的最终颜色
out vec4 finalColor;
uniform vec3 lightPosition; // 光源位置
uniform vec3 lightColor; // 光源颜色
uniform vec3 ambientColor; // 环境光颜色
void main() {
// 计算光照方向
vec3 lightDir = normalize(lightPosition - gl_FragCoord.xyz);
// 计算表面法线和光源方向之间的角度
float diff = max(dot(fragNormal, lightDir), 0.0);
// 基础的漫反射光照模型
vec3 diffuse = diff * lightColor;
// 环境光照
vec3 ambient = ambientColor;
// 最终的颜色是漫反射光加上环境光
finalColor = vec4(ambient + diffuse, 1.0);
}
在这个例子中,顶点着色器将法线和切线传递给片元着色器,片元着色器使用这些信息来进行基本的光照计算。在这个简单的 Lambertian 模型中,计算了光源和表面法线之间的点积,得到漫反射光照分量,并与环境光相加,最终得到片元的颜色。
例子 3:使用特征 ID 进行实例化渲染
假设我们有一个带有实例化的场景,使用 EXT_mesh_gpu_instancing
和 EXT_instance_features
扩展,并根据特征 ID 在每个实例之间应用不同的颜色。
顶点着色器 (custom_vertex_shader.glsl
)
glsl
// 传入的属性
in vec3 positionMC; // 模型坐标系中的位置
in vec4 color_0; // 第一组顶点颜色
in ivec4 joints_0; // 第一组关节索引
in vec4 weights_0; // 第一组权重
// 来自实例化特征 ID 的输入
in int featureId_0; // 特征 ID
// 输出到片元着色器的变量
out vec4 fragColor; // 传递颜色到片元着色器
void main() {
// 使用特征 ID 来修改颜色。例如,可以根据特征 ID 来改变颜色
if (featureId_0 == 0) {
fragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
} else {
fragColor = vec4(0.0, 1.0, 0.0, 1.0); // 绿色
}
// 计算变换后的模型位置
vec4 worldPosition = modelMatrix * vec4(positionMC, 1.0);
gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
片元着色器 (custom_fragment_shader.glsl
)
glsl
// 传入的来自顶点着色器的变量
in vec4 fragColor; // 来自顶点着色器的颜色
// 输出到渲染管线的最终颜色
out vec4 finalColor;
void main() {
// 直接使用传递的颜色作为最终颜色
finalColor = fragColor;
}
在这个例子中,每个实例通过特征 ID 来选择不同的颜色。例如,特征 ID 为 0 的实例渲染为红色,而其他特征 ID 的实例渲染为绿色。通过 featureId_0
,可以灵活地为不同的实例应用不同的效果或属性。