**一、简介** [shadertoy](https://www.shadertoy.com/)是一个基于webgl的分享Shader的开放平台,用户可以在上面根据既定规则分享自己编写的shader。在Cesium中要想实现一些酷炫的效果,唯一的一条路就是写shader,而shader的编写应该算是图形学中的难度天花板了。好在有很多shader大佬分享了自己编写的glsl效果,比如shadertoy网站上就是这些大牛们的作品,我们可以借鉴。  **二、shadertoy着色器基本结构** shadertoy上的shader是纯2d绘图,没有几何顶点这些概念,它的绘图方式和canvas绘图方式很像,它将整个canvas作为绘图的画布,所以输入参数fragCoord的x值范围是(0,画布的宽度),fragCoord的y值范围是(0,画布的高度),画布的宽高在定义的输入参数中是iResolution,所以fragCoord.x范围就是(0,iResolution.x),fragCoord.y范围就是(0,iResolution.y)。  如图所示,shadertoy上的shader示例最基本的着色器结构主要包括两个部分: a、输入参数的定义 b、着色器的入口函数 1、输入参数,通过uniform来定义外部的输入值。 uniform vec3 iResolution; // 视口分辨率,即画布的宽高 uniform float iTime; // shader 的运行时间 秒 uniform float iTimeDelta; // 渲染时间 秒 uniform float iFrameRate; // shader 帧率 uniform int iFrame; // shader 帧率 uniform float iChannelTime[4]; // 频道运行时间 不管 uniform vec3 iChannelResolution[4]; // 频道分辨率 不管 uniform vec4 iMouse; // 鼠标坐标 uniform samplerXX iChannel0..3; // 输入的纹理 比如我们从一张图片上采用颜色 uniform vec4 iDate; // 日期 年月日 不管 uniform float iSampleRate; // 声音采样 不管 2、入口函数mainImage void mainImage(out vec4 fragColor, in vec2 fragCoord ) { fragColor = vec4(1.); } 方法的第一个参数fragColor,是一个vec4类型的变量,表示最后输出的颜色值。 方法的第二个参数fragCoord,是一个vec2类型的变量,表示输入的像素坐标。 **三、在cesium中如何使用** shadertoy上的着色器是在一个canvas画布上进行工作的,要移植到Cesium中,我们需要找一个载体来替代canvas。我们知道,Cesium绘制几何图形可以通过Entity和Primitive两种方式,那么只有Primitive+Appearance比较合适了,关于Primitive的使用及介绍,可以观看前面的章节。要将着色器移植到Cesium中,我们先来重点看看shadertoy上的shader着色器需要用到的参数。以下面这个例子讲解https://www.shadertoy.com/view/XdlSDs  glsl代码: void mainImage(out vec4 fragColor, in vec2 fragCoord ) { vec2 p = (2.0*fragCoord.xy-iResolution.xy)/iResolution.y; float tau = 3.1415926535*2.0; float a = atan(p.x,p.y); float r = length(p)*0.75; vec2 uv = vec2(a/tau,r); //get the color float xCol = (uv.x - (iTime / 3.0)) * 3.0; xCol = mod(xCol, 3.0); vec3 horColour = vec3(0.25, 0.25, 0.25); if (xCol < 1.0) { horColour.r += 1.0 - xCol; horColour.g += xCol; } else if (xCol < 2.0) { xCol -= 1.0; horColour.g += 1.0 - xCol; horColour.b += xCol; } else { xCol -= 2.0; horColour.b += 1.0 - xCol; horColour.r += xCol; } // draw color beam uv = (2.0 * uv) - 1.0; float beamWidth = (0.7+0.5*cos(uv.x*10.0*tau*0.15*clamp(floor(5.0 + 10.0*cos(iTime)), 0.0, 10.0))) * abs(1.0 / (30.0 * uv.y)); vec3 horBeam = vec3(beamWidth); fragColor = vec4((( horBeam) * horColour), 1.0); } a、首先是fragCoord,前面介绍过,fragCoord表示当前处理的像素坐标,是一个vec2类型,fragCoord.x范围为(0,画布宽度),fragCoord.y范围为(0,画布高度)。 b、其次是iResolution,iResolution代表的是当前画布的宽高,即绘图区域的尺寸,所以fragCoord.x范围就是(0,iResolution.x),fragCoord.y范围就是(0,iResolution.y)。 c、然后我们还在代码中看到有个iTime,该参数代表当前运行的时间,一般用来实现动画,因为您会发现大多数shader的效果都是动态的。 d、最后是输出结果fragColor,代表最后计算的颜色输出值,在Cesium中为out\_FragColor。 接下来介绍在Cesium如何获取对应的参数: a、fragCoord在Cesium有个gl\_FragCoord与之对应,这是一个WebGL内置的变量。 b、iResolution在Cesium有个czm\_viewport与之对应,不过使用时采用zw分量即 czm\_viewport.zw c、iTime在Cesium中没有对应的变量,我们可以通过变量的方式传递一个参数,然后在渲染时不断修改该值,不过这种方式略显麻烦,在Cesium中我们可以用float iTime=czm\_frameNumber/100.来模拟,czm\_frameNumber代表当前帧,是一个自增长的数值,所以可以用来模拟时间不断地增长。 接下来我们实操一下,在Cesium中实现该shader的效果: 1、首先我们创建一个Primitive并添加到sene中 let xMin = 115.894604, yMin = 39.516896, xMax = 117.431959, yMax = 40.630521; let rect = new Cesium.Rectangle(Cesium.Math.toRadians(xMin), Cesium.Math.toRadians(yMin), Cesium.Math.toRadians(xMax), Cesium.Math.toRadians(yMax)); const rectangle = new Cesium.RectangleGeometry({ rectangle: rect, height: 10000.0, }); const geometry = Cesium.RectangleGeometry.createGeometry(rectangle); viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: geometry }), })); 2、此时会发现什么也看不见,这是因为没有设置外观,我们创建一个默认的外观 let appearance = new Cesium.MaterialAppearance({ material: new Cesium.Material({ fabric: { type: "Color", uniforms: { color: Cesium.Color.RED } } }), }) primitive.appearance = appearance;  3、接下来我们为外观添加片元着色器,并将shadertoy上的shader赋值给外观的片元着色器属性。 shadertoy上glsl代码: fragmentShaderSource: ` void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 p = (2.0*fragCoord.xy-iResolution.xy)/iResolution.y; float tau = 3.1415926535*2.0; float a = atan(p.x,p.y); float r = length(p)*0.75; vec2 uv = vec2(a/tau,r); //get the color float xCol = (uv.x - (iTime / 3.0)) * 3.0; xCol = mod(xCol, 3.0); vec3 horColour = vec3(0.25, 0.25, 0.25); if (xCol < 1.0) { horColour.r += 1.0 - xCol; horColour.g += xCol; } else if (xCol < 2.0) { xCol -= 1.0; horColour.g += 1.0 - xCol; horColour.b += xCol; } else { xCol -= 2.0; horColour.b += 1.0 - xCol; horColour.r += xCol; } // draw color beam uv = (2.0 * uv) - 1.0; float beamWidth = (0.7+0.5*cos(uv.x*10.0*tau*0.15*clamp(floor(5.0 + 10.0*cos(iTime)), 0.0, 10.0))) * abs(1.0 / (30.0 * uv.y)); vec3 horBeam = vec3(beamWidth); fragColor = vec4((( horBeam) * horColour), 1.0); } ` 操作步骤: a、首先我们需要修改着色器入口函数,即将mainImage修改为main,因为Appearance片元着色器的入口函数是main。 b、然后将输出结果的fragColor修改为out\_FragColor。 c、最后我们按照上面说的替换掉fragCoord,fragCoord、iTime 我们需要的代码: fragmentShaderSource: ` void main() { float iTime=czm_frameNumber/100.; vec2 p = (2.0 * gl_FragCoord.xy-czm_viewport.zw)/czm_viewport.w; float tau = 3.1415926535*2.0; float a = atan(p.x,p.y); float r = length(p)*0.75; vec2 uv = vec2(a/tau,r); //get the color float xCol = (uv.x - (iTime / 3.0)) * 3.0; xCol = mod(xCol, 3.0); vec3 horColour = vec3(0.25, 0.25, 0.25); if (xCol < 1.0) { horColour.r += 1.0 - xCol; horColour.g += xCol; } else if (xCol < 2.0) { xCol -= 1.0; horColour.g += 1.0 - xCol; horColour.b += xCol; } else { xCol -= 2.0; horColour.b += 1.0 - xCol; horColour.r += xCol; } // draw color beam uv = (2.0 * uv) - 1.0; float beamWidth = (0.7+0.5*cos(uv.x*10.0*tau*0.15*clamp(floor(5.0 + 10.0*cos(iTime)), 0.0, 10.0))) * abs(1.0 / (30.0 * uv.y)); vec3 horBeam = vec3(beamWidth); out_FragColor = vec4((( horBeam) * horColour), 1.0); } `  但是移动地球我们会发现绘制的结果始终是在屏幕中心,这其实是一个正确的结果,因为这个示例在shadertoy上也是始终绘制在屏幕中心。现在我们需要将Primitive的几何体作为绘制的画布,上面我们是使用gl\_FragCoord坐标来获取当前应该处理的像素,我们现在需要改变为Appearance的纹理坐标,通过Appearence的纹理坐标,计算当前Appearence上需要处理的像素。 vec2 p = (2.0 * gl_FragCoord.xy-czm_viewport.zw)/czm_viewport.w; 该行代码的意思是将绘图区的宽高转换到[-1,1]的一个区间中。而在Appearance的片元着色器中,相对于该Primitive对应的Geometry而言,绘图区的宽高已经被限制在了[0,1]的区间了,这可以由片元着色器的st推断,因为片元着色器的st一般就是[0,1]。现在我们改造一下代码,将绘图区限定到Appearance的纹理区间中 vec2 p = 2.0 * v_st - 1.;//(2.0*fragCoord.xy-iResolution.xy)/iResolution.y; 因为v\_st区间是[0,1],所以我们的变换一下到[-1,1],运行结果如下  到这里我们已经成功将shadertoy上这个示例的shader移植到Cesium上的Primitve中 **四、着色器使用技巧** 上面示例的shader成功移植到Cesium中的Primitive上。分享2个在使用shadertoy上的shader时的技巧: 1、在Cesium中如何选择对应的载体作为画布 因为shadertoy上的shader类型canvas的绘制原理,是将canvas作为一个画布,在Cesium中我们可以选择Entity或Primtive来作为载体。又因为要方便操作片元着色器,所以我们选择了Primitive,但是Primitive中又有很多Geometry类型,那具体使用哪种Geometry呢?根据经验,最好选择像RectangleGeometry、PlaneGeomery这种比较规则的几何类型,因为shader绘图其实是根据纹理坐标来实现的,而这种规则的几何往往 它的纹理坐标也比较规则。 2、如何去除黑色背景 虽然我们已经成功将shader移植到Cesium中,但是黑色的背景着实有点丑,我们使用shader的初衷是为了好看、酷炫,这效果好像有点违背了我们的初衷。那我们该如何去除这个黑色的背景呢?我们可以分析一下这个黑色,其实黑色的值就是vec3(0,0,0),rgb三个分量越接近于0,就越黑,当然,这也代表着r+g+b约接近0,所以我们可以这样去消除黑色背景。 out_FragColor = vec4(color,color.r+color.g+color.g);  这样就可以了。 **五、难度加深,带有输入纹理数据的着色器示例(应用)** 实现一个[shader](https://www.shadertoy.com/view/ldfyzl)上带有输入纹理数据的例子  按照 “在Cesium中如何使用” 一节的思路,我们先创建好Primitive、Appearance,然后将着色器代码拷贝进来并修改相关参数。 let xMin = 115.894604, yMin = 39.516896, xMax = 117.431959, yMax = 40.630521; const rectangle = new Cesium.RectangleGeometry({ rectangle: rect, height: 10000.0, }); const geometry = Cesium.RectangleGeometry.createGeometry(rectangle); viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: geometry }), appearance: new Cesium.MaterialAppearance({ material:new Cesium.Material({ fabric: { uniforms: { image:"./texture.png" } } }) , fragmentShaderSource: ` in vec3 v_positionEC; in vec3 v_normalEC; in vec2 v_st; // Maximum number of cells a ripple can cross. #define MAX_RADIUS 2 // Set to 1 to hash twice. Slower, but less patterns. #define DOUBLE_HASH 0 // Hash functions shamefully stolen from: // https://www.shadertoy.com/view/4djSRW #define HASHSCALE1 .1031 #define HASHSCALE3 vec3(.1031, .1030, .0973) float hash12(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * HASHSCALE1); p3 += dot(p3, p3.yzx + 19.19); return fract((p3.x + p3.y) * p3.z); } vec2 hash22(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * HASHSCALE3); p3 += dot(p3, p3.yzx+19.19); return fract((p3.xx+p3.yz)*p3.zy); } void main( ) { //float resolution = 10. * exp2(-3.*iMouse.x/iResolution.x); // vec2 uv = fragCoord.xy / iResolution.y * resolution; float resolution =20.; vec2 uv = v_st * resolution; vec2 p0 = floor(uv); float iTime=czm_frameNumber/100.; vec2 circles = vec2(0.); for (int j = -MAX_RADIUS; j <= MAX_RADIUS; ++j) { for (int i = -MAX_RADIUS; i <= MAX_RADIUS; ++i) { vec2 pi = p0 + vec2(i, j); #if DOUBLE_HASH vec2 hsh = hash22(pi); #else vec2 hsh = pi; #endif vec2 p = pi + hash22(hsh); float t = fract(0.3*iTime + hash12(hsh)); vec2 v = p - uv; float d = length(v) - (float(MAX_RADIUS) + 1.)*t; float h = 1e-3; float d1 = d - h; float d2 = d + h; float p1 = sin(31.*d1) * smoothstep(-0.6, -0.3, d1) * smoothstep(0., -0.3, d1); float p2 = sin(31.*d2) * smoothstep(-0.6, -0.3, d2) * smoothstep(0., -0.3, d2); circles += 0.5 * normalize(v) * ((p2 - p1) / (2. * h) * (1. - t) * (1. - t)); } } circles /= float((MAX_RADIUS*2+1)*(MAX_RADIUS*2+1)); float intensity = mix(0.01, 0.15, smoothstep(0.1, 0.6, abs(fract(0.05*iTime + 0.5)*2.-1.))); vec3 n = vec3(circles, sqrt(1. - dot(circles, circles))); vec3 color = texture(image_0, uv/resolution - intensity*n.xy).rgb + 5.*pow(clamp(dot(n, normalize(vec3(1., 0.7, 0.5))), 0., 1.), 6.); out_FragColor = vec4(color, 1.0); } ` }) })); 分析代码: float resolution = 10. * exp2(-3.*iMouse.x/iResolution.x); vec2 uv = fragCoord.xy / iResolution.y * resolution; 意思是将uv转换到[0,x]的区间,在Cesium中应该这样写,我们先固定一个值 float resolution =20.; vec2 uv = v_st * resolution; 本节的重点是 vec3 color = texture(iChannel0, uv/resolution - intensity*n.xy).rgb + 5.*pow(clamp(dot(n,normalize(vec3(1., 0.7, 0.5))), 0., 1.), 6.); texture(iChannel0, uv/resolution - intensity*n.xy).rgb 从纹理中采样颜色值,这个iChannel0是sampler2D,在Cesium对应Cesium.Texture,不过我们可以直接传一张图片,Cesium会自动为我们封装成Cesium.Texture。将图片传递到着色器,这里我们采用Materail的属性uniforms,需要注意的是如果您在Appearance的片元着色器中使用Material中的uniforms参数值时,你必须在参数名的后面加上一个序号,比如你的第一个参数为在Material的uniforms中为image,那在Appearance的片元着色器中必须使用image\_0,如下: texture(image_0, uv/resolution - intensity*n.xy).rgb  **六、Perlin-Worley噪声实现云图** shadertoy示例地址:https://www.shadertoy.com/view/3dVXDc  该示例使用了Perlin+Worley(柏林+沃利)噪声来实现云的效果,并且是先将结果绘制到一张Texture中,然后在主函数中根据纹理坐标读取纹理中的值。 1、首先是噪声函数,定义在Common中  2、然后是绘制到Texture中,过程在BufferA中  3、最后在主函数中调用  4、主函数中为了显示了各个噪声的结果,将画布拆分为了5份,我们可以注释掉一些不需要的结果显示,比如修改代码如下: void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 st = fragCoord / iResolution.xy; vec2 uv = fragCoord / iResolution.y; st.x *= 5.; // 5 columns for different noises uv -= .02 * iTime; vec3 col = vec3(0.); float perlinWorley = textureLod(iChannel0, uv * .5, 0.).x; // worley fbms with different frequencies vec3 worley = textureLod(iChannel0, uv, 0.).yzw; float wfbm = worley.x * .625 + worley.y * .125 + worley.z * .25; // cloud shape modeled after the GPU Pro 7 chapter float cloud = remap(perlinWorley, wfbm - 1., 1., 0., 1.); cloud = remap(cloud, .85, 1., 0., 1.); // fake cloud coverage // if (st.x < 1.) // col += perlinWorley; // else if(st.x < 2.) // col += worley.x; // else if(st.x < 3.) // col += worley.y; // else if(st.x < 4.) // col += worley.z; // else if(st.x < 5.) col += cloud; // column dividers // float div = smoothstep(.01, 0., abs(st.x - 1.)); // div += smoothstep(.01, 0., abs(st.x - 2.)); // div += smoothstep(.01, 0., abs(st.x - 3.)); // div += smoothstep(.01, 0., abs(st.x - 4.)); // col = mix(col, vec3(0., 0., .866), div); fragColor = vec4(col,1.0); } 因为我们不需要绘制到Texture的步骤,所以需要合并BufferA和主函数的代码 uv -= .02 * iTime; vec2 m = vec2(0.5); vec4 col = vec4(0.); float slices = 128.; // number of layers of the 3d texture float freq = 4.; float pfbm= mix(1., perlinfbm(vec3(uv, floor(m.y*slices)/slices), 4., 7), .5); pfbm = abs(pfbm * 2. - 1.); // billowy perlin noise col.g += worleyFbm(vec3(uv, floor(m.y*slices)/slices), freq); col.b += worleyFbm(vec3(uv, floor(m.y*slices)/slices), freq*2.); col.a += worleyFbm(vec3(uv, floor(m.y*slices)/slices), freq*4.); col.r += remap(pfbm, 0., 1., col.g, 1.); // perlin-worley float perlinWorley = col.r; // worley fbms with different frequencies vec3 worley = col.yzw; float wfbm = worley.x * .625 + worley.y * .125 + worley.z * .25; // cloud shape modeled after the GPU Pro 7 chapter float cloud = remap(perlinWorley, wfbm - 1., 1., 0., 1.); cloud = remap(cloud, .65, 1., 0., 1.); // fake cloud coverage vec3 col_ = vec3(0.); col_ += cloud; 5、在cesium中如何实现:我们先创建好Primitive、Appearance,然后将着色器代码拷贝进来并修改相关参数 let xMin = 115.894604, yMin = 39.516896, xMax = 117.431959, yMax = 40.630521; const rectangle = new Cesium.RectangleGeometry({ rectangle: rect, height: 10000.0, }); const geometry = Cesium.RectangleGeometry.createGeometry(rectangle); viewer.scene.primitives.add(new Cesium.Primitive({ geometryInstances: new Cesium.GeometryInstance({ geometry: geometry }), appearance: new Cesium.MaterialAppearance({ material:new Cesium.Material({ fabric: { uniforms: { image:"./texture.png" } } }) , fragmentShaderSource: ` in vec3 v_positionEC;//顶点 相机(眼睛)坐标系 in vec3 v_normalEC;//顶点法线 相机(眼睛)坐标系 in vec2 v_st;//纹理坐标 uv /** This tab contains all the necessary noise functions required to model a cloud shape. */ // Hash by David_Hoskins #define UI0 1597334673U #define UI1 3812015801U #define UI2 uvec2(UI0, UI1) #define UI3 uvec3(UI0, UI1, 2798796415U) #define UIF (1.0 / float(0xffffffffU)) vec3 hash33(vec3 p) { uvec3 q = uvec3(ivec3(p)) * UI3; q = (q.x ^ q.y ^ q.z)*UI3; return -1. + 2. * vec3(q) * UIF; } float remap(float x, float a, float b, float c, float d) { return (((x - a) / (b - a)) * (d - c)) + c; } // Gradient noise by iq (modified to be tileable) float gradientNoise(vec3 x, float freq) { // grid vec3 p = floor(x); vec3 w = fract(x); // quintic interpolant vec3 u = w * w * w * (w * (w * 6. - 15.) + 10.); // gradients vec3 ga = hash33(mod(p + vec3(0., 0., 0.), freq)); vec3 gb = hash33(mod(p + vec3(1., 0., 0.), freq)); vec3 gc = hash33(mod(p + vec3(0., 1., 0.), freq)); vec3 gd = hash33(mod(p + vec3(1., 1., 0.), freq)); vec3 ge = hash33(mod(p + vec3(0., 0., 1.), freq)); vec3 gf = hash33(mod(p + vec3(1., 0., 1.), freq)); vec3 gg = hash33(mod(p + vec3(0., 1., 1.), freq)); vec3 gh = hash33(mod(p + vec3(1., 1., 1.), freq)); // projections float va = dot(ga, w - vec3(0., 0., 0.)); float vb = dot(gb, w - vec3(1., 0., 0.)); float vc = dot(gc, w - vec3(0., 1., 0.)); float vd = dot(gd, w - vec3(1., 1., 0.)); float ve = dot(ge, w - vec3(0., 0., 1.)); float vf = dot(gf, w - vec3(1., 0., 1.)); float vg = dot(gg, w - vec3(0., 1., 1.)); float vh = dot(gh, w - vec3(1., 1., 1.)); // interpolation return va + u.x * (vb - va) + u.y * (vc - va) + u.z * (ve - va) + u.x * u.y * (va - vb - vc + vd) + u.y * u.z * (va - vc - ve + vg) + u.z * u.x * (va - vb - ve + vf) + u.x * u.y * u.z * (-va + vb + vc - vd + ve - vf - vg + vh); } // Tileable 3D worley noise float worleyNoise(vec3 uv, float freq) { vec3 id = floor(uv); vec3 p = fract(uv); float minDist = 10000.; for (float x = -1.; x <= 1.; ++x) { for(float y = -1.; y <= 1.; ++y) { for(float z = -1.; z <= 1.; ++z) { vec3 offset = vec3(x, y, z); vec3 h = hash33(mod(id + offset, vec3(freq))) * .5 + .5; h += offset; vec3 d = p - h; minDist = min(minDist, dot(d, d)); } } } // inverted worley noise return 1. - minDist; } // Fbm for Perlin noise based on iq's blog float perlinfbm(vec3 p, float freq, int octaves) { float G = exp2(-.85); float amp = 1.; float noise = 0.; for (int i = 0; i < octaves; ++i) { noise += amp * gradientNoise(p * freq, freq); freq *= 2.; amp *= G; } return noise; } // Tileable Worley fbm inspired by Andrew Schneider's Real-Time Volumetric Cloudscapes // chapter in GPU Pro 7. float worleyFbm(vec3 p, float freq) { return worleyNoise(p*freq, freq) * .625 + worleyNoise(p*freq*2., freq*2.) * .25 + worleyNoise(p*freq*4., freq*4.) * .125; } void main( ) { vec2 uv = v_st; float iTime=czm_frameNumber/100.; uv -= .02 * iTime; vec2 m = vec2(0.5); vec4 col = vec4(0.); float slices = 128.; // number of layers of the 3d texture float freq = 4.; float pfbm= mix(1., perlinfbm(vec3(uv, floor(m.y*slices)/slices), 4., 7), .5); pfbm = abs(pfbm * 2. - 1.); // billowy perlin noise col.g += worleyFbm(vec3(uv, floor(m.y*slices)/slices), freq); col.b += worleyFbm(vec3(uv, floor(m.y*slices)/slices), freq*2.); col.a += worleyFbm(vec3(uv, floor(m.y*slices)/slices), freq*4.); col.r += remap(pfbm, 0., 1., col.g, 1.); // perlin-worley float perlinWorley = col.r; // worley fbms with different frequencies vec3 worley = col.yzw; float wfbm = worley.x * .625 + worley.y * .125 + worley.z * .25; // cloud shape modeled after the GPU Pro 7 chapter float cloud = remap(perlinWorley, wfbm - 1., 1., 0., 1.); cloud = remap(cloud, .65, 1., 0., 1.); // fake cloud coverage vec3 col_ = vec3(0.); col_ += cloud; out_FragColor = vec4(col_,col.r); ` }) })); 这样就实现了体积云类似效果  **七、流体水面** 示例地址:https://www.shadertoy.com/view/4dd3Rl  取到代码在cesium中如何使用参考上面体积云的示例,步骤相同。