cesium编程中级(十六)cesium-texture3d
来由
今天来学习沙盒中的示例 Volumetric Cloud,其中用到了 Texture3D,Texture3D是1.132 版本中新增的功能。
之前也有过一个云的示例: cloud parameters,使用Cesium.CloudCollection 来实现的,本质上是构建了一个云的二维 texture,然后去渲染,所以旋转的时候,发现像是一个billboard一样,一直只有一面对着你,感觉不够”三维”,
Volumetric Cloud不一样,旋转之后是可以看出变化的,感觉上是真的三维。
介绍
这个示例很长,610行代码(Cesium1.136版本),由几个部分组成,里面有一些注释,我们来捋一遍:
- 第1行,引入Cesium;
- 第2~111行,引用开源代码(three.js),生成噪声,会用就行了;
- 第112~202行,构建自定义GeometryPrimitive(值得学习);
- 第203~260行,构建Texture3D(makeTexture3D);
- 第261~385行,顶点和片元着色器(重点学习);
- 第386~495行,构建场景,调用之前的GeometryPrimitive,有基础的扫一眼就明白了;
- 第496~610行,构建左上角的交互控件和样式,不是重点;
那么重点很明显了:
- 学习构建 自定义GeometryPrimitive
- 学习调用开源代码生成Texture3D需要的数据,那么以后再来Texture4D,Texture5D…,一样的调用,毕竟自己写这个噪点生成的代码,要求太高;
- 功能实现的顶点和片元着色器代码;
重点
1. 自定义GeometryPrimitive
GeometryPrimitive 继承自 geometry,需要实现主要方法,我们把骨架抽出来,
class GeometryPrimitive {
constructor(geometry, options) {
this.options = options;
this.geometry = geometry;
}
update(frameState) {
//.....
//构建DrawCommand
this._drawCommand = new DrawCommand({
//....
});
}
destroy() {
return destroyObject(this);
}
isDestroyed() {
return false;
}
}
调用的地方:
const primitive = new GeometryPrimitive(boxGeometry, {
uniformMap: cmdUniforms,
vertexShaderSource: vertexShader,
fragmentShaderSource: fragmentShader,
renderState: renderState,
modelMatrix,
pass: Cesium.Pass.TRANSLUCENT,
});
这个样子看,是不是没有那么难了,我们以后自己也可以照这个套路写自定义的GeometryPrimitive
2. 生成Texture3D
这里构建了一个长度为 层行列 的数据,用三阶循环填充,可以理解成想组织的数据是尺寸相同,叠在一起的多张相片,把每一行用剪刀裁剪开,然后拼成一个长条。数据通过Cesium.Texture3D封装
new Cesium.Texture3D({
context: context,
width: size,
height: size,
depth: size,
flipY: false,
pixelFormat: Cesium.PixelFormat.RED,
pixelDatatype: Cesium.PixelDatatype.UNSIGNED_BYTE,
source: {
arrayBufferView: data, //传入数据
width: size,
height: size,
depth: size,
},
sampler: new Cesium.Sampler({
minificationFilter: Cesium.TextureMinificationFilter.LINEAR,
magnificationFilter: Cesium.TextureMagnificationFilter.LINEAR,
}),
});
//...
const texture3D = makeTexture3D(viewer.scene.context);
// 调用更底层的 gl.generateMipmap 方法
texture3D.generateMipmap();
/*
Cesium-1.136\packages\engine\Source\Renderer\Texture3D.js
Texture3D.prototype.generateMipmap = function (hint) {
//...
const gl = this._context._gl;
const target = this._textureTarget;
gl.hint(gl.GENERATE_MIPMAP_HINT, hint);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(target, this._texture);
gl.generateMipmap(target);
gl.bindTexture(target, null);
};
*/
到这里,数据准备完成
3. 着色器
顶点着色器
通过顶点着色器获取了两个信息,相机的位置和朝向
片元着色器
- 求交 hitBox(…) 计算射线与单位立方体 [±0.5] 的入口 t0 和出口 t1,没有交点就 discard;
- 插值 步长 delta = 1 /steps(steps由外部传入,这里是100),就是将 t0 和 t1 的连线插值100层;
- 采样 sample1(p) 直接 texture(map, p).r 拿值; shading(…) 用中心差分算梯度;
- 体渲染 阈值 threshold、软过渡 range、不透明度 opacity 都是外部 uniform; 前面累积颜色 ac,前面乘 (1-ac.a) 做正面混合,alpha>0.95 提前跳出。
- 后处理 linearToSRGB + Cesium 内置 czm_gammaCorrect 做伽玛校正,最后 out_FragColor 输出。
整个代码都是从threejs那边借鉴过来,所以其实我觉得关注点应该在于,着色器移植应该注意什么问题,也许更贴合实际一点
后记
之前我自己也花了不少时间做过这个示例的迁移,虽然出了一个版本,但是总感觉做的不好,当时的纹理是通过Cesium.Texture 传入的,因为当时还没有Cesium.Texture3D,看到这个示例,感觉很亲切。