Cesium水面渲染(一)


前言

水面渲染大体有三种技术路线:平面反射、顶点动画、流体模拟,考虑到性能问题,这里使用平面反射的方式实现。和常规物体的渲染方式不同,水面的渲染我们除了要处理反射,还需要处理折射的内容。

对于水面的反射处理,通常有以下几种方式:平面反射(利用镜像相机)、屏幕空间反射(SSR)、立方体贴图反射、光线追踪。其性能对比如下:

技术CPU开销GPU开销显存占用
平面反射中高
SSR
立方体贴图极低
光线追踪极高

平面反射和屏幕空间反射是用的比较多的一种,但是平面反射占用的资源比较多,而且通常用于平坦的平面,而屏幕空间反射在一定视角下不能反射天空(比如垂直视角),立方体贴图虽然占用的资源少,但是因为只适用于静态的,一般情况下比较少使用。这里我们结合屏幕空间反射和立方体贴图,一起使用。

具体来说就是,使用屏幕空间反射来反射主体,对于天空,我们采用动态立方体贴图的方式反射,来避免一定视角下无法反射物体的问题。

一、动态天空盒

我们第一步需要先实现动态天空盒,Cesium有内置的SkyBox和内置的环境反射贴图(czm_environmentMap),但这个并不是很好,我们根据https://www.shadertoy.com/view/4tdSWr,这里的效果重新制作一个天空盒。

1.云的渲染

它这里面的云是二维的,我们要如何转成三维的呢,这里我们参考Cesium的大气渲染实现的逻辑即可,Cesium的大气渲染(SkyAtmosphere)是使用一个球体承载渲染的。我们只要把它调整一下即可,重点调整的地方在update方法。

update(frameState, globe) {
    if (!this.show) {
      return undefined;
    }

    const mode = frameState.mode;
    if (mode !== SceneMode.SCENE3D && mode !== SceneMode.MORPHING) {
      return undefined;
    }

    // The atmosphere is only rendered during the render pass; it is not pickable, it doesn't cast shadows, etc.
    if (!frameState.passes.render) {
      return undefined;
    }


    // Align the ellipsoid geometry so it always faces the same direction as the
    // camera to reduce artifacts when rendering atmosphere per-vertex
    const rotationMatrix = Matrix4.fromRotationTranslation(
      frameState.context.uniformState.inverseViewRotation,
      Cartesian3.ZERO,
      scratchModelMatrix,
    );
    const rotationOffsetMatrix = Matrix4.multiplyTransformation(
      rotationMatrix,
      Axis.Y_UP_TO_Z_UP,
      scratchModelMatrix,
    );
    const modelMatrix = Matrix4.multiply(
      this._scaleMatrix,
      rotationOffsetMatrix,
      scratchModelMatrix,
    );
    Matrix4.clone(modelMatrix, this._modelMatrix);

    const context = frameState.context;

    const translucent = frameState.globeTranslucencyState.translucent;
    const perFragmentAtmosphere =
      this.perFragmentAtmosphere || translucent || !defined(globe) || !globe.show;

    const command = this._command;

    if (!defined(command.vertexArray)) {
      const geometry = EllipsoidGeometry.createGeometry(
        new EllipsoidGeometry({
          radii: new Cartesian3(1.0, 1.0, 1.0),
          //slicePartitions: 256,
          //stackPartitions: 256,
          vertexFormat: VertexFormat.POSITION_ONLY,
        }),
      );
      command.vertexArray = VertexArray.fromGeometry({
        context: context,
        geometry: geometry,
        attributeLocations: GeometryPipeline.createAttributeLocations(geometry),
        bufferUsage: BufferUsage.STATIC_DRAW,
      });
      command.renderState = RenderState.fromCache({
        cull: {
          enabled: true,
          face: CullFace.FRONT,
        },
        blending: BlendingState.ALPHA_BLEND,
        depthMask: false,
      });
      command.uniformMap = this.uniformMap;
    }

    if (this._texture == undefined || this.updateTexture) {
      this.updateTexture = false;
      //this.createTexture(context,false);
    }


    const flags = (translucent << 3);

    if (flags !== this._flags) {
      this._flags = flags;

      const defines = [];


      if (translucent) {
        defines.push("GLOBE_TRANSLUCENT");
      }

      const vs = new ShaderSource({
        defines: defines,
        sources: [SkyCloudVS],
      });

      const fs = new ShaderSource({
        defines: defines,
        sources: [SkyCloudFS],
      });

      this._spSkyCloud = ShaderProgram.fromCache({
        context: context,
        vertexShaderSource: vs,
        fragmentShaderSource: fs,
      });

      command.shaderProgram = this._spSkyCloud;
    }


    return command;
  }

这里的SkyCloudFS就是我们的片元着色器,我们直接照搬shadertoy的就好,这里有一个地方需要调整一下,在shadertoy中,他是根据屏幕坐标生成的uv采样,他是使用noise生成的2d噪声,但我们需要根据顶点生成uv,所以我们需要调整成3d的版本。

// 3D Simplex Noise 核心函数
float noise3(vec3 p) {
    // 三维晶格常数 (3D simplex math constants)
    const float F3 = 1.0/3.0;
    const float G3 = 1.0/6.0;
    
    // 确定四面体单元 (Determine simplex cell)
    vec3 s = floor(p + dot(p, vec3(F3)));
    vec3 x = p - s + dot(s, vec3(G3));
    
    // 确定四面体子单元 (Permute to find tetrahedron)
    vec3 e = step(vec3(0.0), x - x.yzx);
    vec3 i1 = e*(1.0 - e.zxy);
    vec3 i2 = 1.0 - e.zxy*(1.0 - e);
    
    // 计算六个中间点坐标
    vec3 x1 = x - i1 + G3;
    vec3 x2 = x - i2 + 2.0*G3;
    vec3 x3 = x - 1.0 + 3.0*G3;
    
    // 哈希梯度生成 (使用三维哈希)
    vec4 h = hash43(s);
    vec4 j = hash43(s + i1);
    vec4 k = hash43(s + i2);
    vec4 l = hash43(s + 1.0);
    
    // 计算四个顶点的贡献
    vec4 m = max(0.6 - vec4(dot(x,x), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
    vec4 m2 = m * m;
    vec4 m4 = m2 * m2;
    
    // 梯度点积计算
    vec4 pdot = vec4(
        dot(grad3[int(h.w)%12], x),
        dot(grad3[int(j.w)%12], x1),
        dot(grad3[int(k.w)%12], x2),
        dot(grad3[int(l.w)%12], x3)
    );
    
    // 综合贡献值 (调整振幅系数为32.0)
    return 32.0 * dot(m4, pdot);
}

调整之后,我们可以得到类似这样的效果
在这里插入图片描述
这是shadertoy的效果
在这里插入图片描述

2.融合大气渲染

两个大体上形状已经差不多了,但颜色并不正常,接下来,我们需要在融合Cesium的大气效果,整合到云的光照计算中。Cesium的大气渲染是由environmentState.skyAtmosphereCommand执行得到,所以我们并不需要在云的计算中再次计算大气效果,我们只需要先将skyAtmosphereCommand渲染到一个临时的fbo,再将此fbo的结果作为纹理传递给云的绘制命令即可。

拿到大气的渲染纹理后,接下里的事情就很好办了,云的渲染网上的资料已经很多了,这里就不展开讲,感兴趣的可以参考这篇文章https://zhuanlan.zhihu.com/p/248406797,这里虽然讲的是体积云,我们仅仅只是采样一层3d噪声来渲染,但原理大体是一致的。

大体流程是:
根据云的密度计算光照衰减

float beerTransmittance = exp(-cloudDensity * u_extinctionCoeff);

根据太阳和视线方向的夹角计算太阳的光照,模拟不同时间的阳光

float sunDot = dot(sunDirection, upDir);
float horizonFactor = pow(1.0 - sunDot, 2.0);
vec3 sunIntensity = mix(vec3(0.93, 0.97, 1.0), vec3(0.99, 0.86, 0.69), horizonFactor);

混合太阳光和大气散色的颜色

vec3 ambientLight = (texture(u_atmosphereTexture,suv).rgb*0.6+directLight*0.7)* beerTransmittance;
ambientLight = directLight* beerTransmittance*u_sunCoeff;

结合云基础色混合成最终的颜色,完整的光照计算代码如下

vec4 computeCloudLighting(float cloudDensity, vec3 sp, vec3 sunDir,vec2 suv) {
    if(cloudDensity<0.01)
    {
        return vec4(0.0,0.0,0.0,0.0);
    }

    cloudDensity = min(cloudDensity, 0.8);
    vec3 cameraToPositionWC = sp - czm_viewerPositionWC;
    vec3 spDir =normalize(cameraToPositionWC);

    // Henyey-Greenstein相位函数(模拟云层各向异性散射)
    vec3 phaseSunDir = normalize(czm_sunPositionWC - sp);
    float phase = phaseFunction(dot(spDir, phaseSunDir),0.5); 

    // Beer-Lambert定律计算光衰减(密度积分)
    float beerTransmittance = exp(-cloudDensity * u_extinctionCoeff);

    vec3 sunDirection = normalize(sunDir);
    vec3 upDir = normalize(czm_viewerPositionWC);
    float sunDot = dot(sunDirection, upDir);
    float horizonFactor = pow(1.0 - sunDot, 2.0);
    vec3 sunIntensity = mix(vec3(0.93, 0.97, 1.0), vec3(0.99, 0.86, 0.69), horizonFactor);

    vec3 directLight = sunIntensity * phase;
    vec3 ambientLight = (texture(u_atmosphereTexture,suv).rgb*0.6+directLight*0.7)* beerTransmittance;
    ambientLight = directLight* beerTransmittance*u_sunCoeff;
    
    sunDot =step(0.2,sunDot) + (sunDot/0.2);
    sunDot = clamp(sunDot,0.0,1.0);
    vec3 cloudWhite =mix(u_cloudColorMin,u_cloudColorMax,sunDot);
    vec3 scatteredWhite = cloudWhite * (1.0 - beerTransmittance);
    vec3 resultColor = ambientLight + scatteredWhite;


    return vec4(resultColor, 1.0-beerTransmittance);
}

最终我们得到这样的效果
在这里插入图片描述

3.生成动态立方体贴图

到这里我们已经完成了动态的天空盒渲染,接下来我们需要把结果存储到CubeMap里面,以便在水面渲染的时候能采样。在Cesium中使用CubeMap类表示立方体贴图,我们可以参考SkyBox的代码创建动态的CubeMap。

主要流程如下:
创建默认的CubeMap,大小是256
根据放置点创建局部坐标系。比如东北天坐标系(ENU),设置相机初始方向。
获取CubeMap中的fbo,根据fbo更新相机的方向
渲染大气和云到不同的fbo中。

主要代码如下

generateCubeMap(context, position) {
        this.setViewport(this.scene.context, this.textureSize, this.textureSize);

        let frameNumberIndex = this.scene.frameState.frameNumber%6;
        this.coordinateArray.splice();
        let py = this.getPY(position);
        this.coordinateArray.push(py);
        this.coordinateArray.push(this.getNY(py));
        this.coordinateArray.push(this.getPX(py));
        this.coordinateArray.push(this.getNX(py));
        this.coordinateArray.push(this.getPZ(py));
        this.coordinateArray.push(this.getNZ(py));

        this.renderToFbo(this.coordinateArray[frameNumberIndex], this.fboArray[frameNumberIndex]);

        this.resetViewport(this.scene.context);
    }
renderToFbo(cameraInfo, fbo) {
        viewer.camera.right = cameraInfo.right;
        viewer.camera.up = cameraInfo.up;
        viewer.camera.direction = cameraInfo.direction;
        viewer.camera.updateMembers(viewer.camera);
        viewer.scene.context.uniformState.updateCamera(viewer.camera);
        this.render(fbo);
    }

render(framebuffer) {
        let environmentState = this.scene.environmentState;
        if (environmentState.isSkyAtmosphereVisible) {
            let skyAtmosphereCommand = environmentState.skyAtmosphereCommand;
            skyAtmosphereCommand.framebuffer = framebuffer;
            skyAtmosphereCommand.execute(this.scene.context, this.passState);
            skyAtmosphereCommand.framebuffer = undefined;
        }
        if(!this.scene.skyCloud.show){
            return;
        }

        if (environmentState.isSkyAtmosphereVisible && environmentState.skyFrameBuffer != undefined) {
            let skyCloudCommand = environmentState.skyCloudCommand;
            skyCloudCommand.framebuffer = framebuffer;
            skyCloudCommand.execute(this.scene.context, this.passState);
            skyCloudCommand.framebuffer = undefined;
        }
    }

在每一帧我们只渲染立方体的一个面,在每次渲染完毕之后,我们需要恢复相机原始的参数,具体的实现逻辑可以参考ThreeJS的CubeCamera,这样我们就得到了动态天空盒的立方体贴图了。渲染出来结果如下。

在这里插入图片描述

二、折射贴图

水面还有折射效果,如果说水面的反射贴图是使用镜像相机的话,那么折射贴图使用的相机则是和当前相机一样,那么 我们只需要从Cesium的主fbo中读取渲染结果即可。

getRefractionMapFbo(scene,passState,sourceFrambuffer){
        let context = scene.context;
        const framebuffer = new Framebuffer({
            context: context,
            colorTextures: [
                new Texture({
                    context: scene.context,
                    width: scene.context.drawingBufferWidth,
                    height: scene.context.drawingBufferHeight,
                    pixelFormat: PixelFormat.RGBA,
                }),
            ],
        });

        let fs = `
          uniform highp sampler2D u_Texture;

          in vec2 v_textureCoordinates;

          void main()
          {
           vec4 color = texture(u_Texture, v_textureCoordinates);
            out_FragColor = color;
          }
          `

        let copyColorCommand = scene.context.createViewportQuadCommand(
            fs, {
                uniformMap: {
                    u_Texture: function () {
                        return sourceFrambuffer.getColorTexture(0);
                    },
                }
            },
        );
        copyColorCommand.framebuffer = framebuffer;
        copyColorCommand.execute(scene.context,passState);
        return framebuffer;
    }

三、水面渲染

拿到了反射贴图和折射贴图,那么基本上已经完成最难的部分,接下来,我们只需要整合即可,渲染的shder我们可以直接参考threeJS的效果https://threejs.org/examples/#webgl_water。把里面的反射贴图和折射贴图替换成我们自己的,这里需要注意的是,threeJS的折射贴图和反射贴图都是使用的2d贴图,并没有使用立方体贴图。

threeJS里面并没有高光反射,这里我们需要补充一下,我们直接使用 Blinn-Phong 模型

vec3 calculateSpecular(vec3 worldPos, vec3 normal, vec3 viewDir,vec3 sunColor,vec3 specularColor) {
    vec3 lightDir = normalize(czm_sunPositionWC -worldPos);
    vec3 halfVec = normalize(lightDir + viewDir);
    float NdotH = max(dot(normal, halfVec), 0.0);
    float shininess = 8.0; // 扩展光滑度范围
    float spec = pow(NdotH, shininess);
    return sunColor * 0.5 * spec;
}

整合之后,主要代码如下

	vec3 normalOffset = getRoNormal(vec2(uu,vv),u_Scale,vFlowRate);
    vec3 reNormal = normalize(normalOffset + n);

	float theta = max( dot(v, reNormal ), 0.0 );
    float reflectivity = 0.02;
	float reflectance = reflectivity + ( 1.0 - reflectivity ) * pow( ( 1.0 - theta ), 5.0 );

    vec3 skyNormal = vec3(normalOffset.x*0.05*gl_FragCoord.z,normalOffset.x*0.05*gl_FragCoord.z,1.0);
    skyNormal = normalize(skyNormal);
    vec3 reflectColor = getSkyColor(worldP,skyNormal);
    vec3 refractColor = getWaterRefraction(normalOffset,gl_FragCoord.xyz);
    vec3 outColor = mix( refractColor, reflectColor, reflectance );

    vec3 sunDirection = czm_sunPositionWC - czm_viewerPositionWC;
    sunDirection = normalize(sunDirection);
    vec3 upDir = normalize(czm_viewerPositionWC);
    float sunDot = dot(sunDirection, upDir);
    float horizonFactor = pow(1.0 - sunDot, 2.0);
    vec3 sunIntensity = mix(vec3(0.93, 0.97, 1.0), vec3(0.99, 0.86, 0.69), horizonFactor);

    vec3 specularNormal = normalize(normalOffset*0.05 + n);
    vec3 specularColor = calculateSpecular(worldP,specularNormal,v,sunIntensity,reflectColor);
    outColor+=specularColor*reflectance;
    out_FragColor = vec4(outColor, 1.0);

有些地方和threeJS存在一点差异,但整体逻辑是一致的。最后我们得到这样的效果

在这里插入图片描述
在这里插入图片描述

总结

这里的立方体贴图我们仅仅存储了天空的渲染结果,其实还可以进一步简化,比如不使用立方体贴图了,直接使用镜像相机渲染天空,以此作为反射贴图,效果也不会很差。如果把场景中所有的物体都渲染到立方体贴图,那么基本上就是反射探针实现的逻辑了,这样渲染的压力很大。

目前实现的效果作为第一个版本还可以,但有些地方还存在问题,比如天空的云、大气、太阳效果并不统一,云的渲染逻辑并不符合真实的渲染方程,能量不守恒。水面的渲染还有很多地方没有完善,比如泡沫,水深等,只能说作为一个粗糙的版本还是可以的。下一步我们再叠加屏幕空间反射,进一步完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值