一、回顾
上一篇我们将了如何在后处理中获取世界坐标还有高程,还了解了两个简单的线性雾,从上一篇的线性高度雾我们可以发现,一旦相机不是大角度俯视,那么雾的表现就会失真,如果相机进入雾的范围则更假,因为雾气只是简单的贴在物体和地形上,并不会对整个视野造成影响。
二、浓度积分高度雾公式推导
1.为什么用积分
下图中,雾气浓度随着高程增加不断变低(先不用管怎么变),对于同一个点,我们从三个不同的角度观看,离目标点的距离都是相等的,那么最终看到的目标颜色会相同吗?或者说这个点最终的雾值会一样吗?其实我们都知道它们应该是不一样的,并且 F2 < F1 < F3 。情况3下视线被雾气遮挡的最多,情况1则最少。
那么怎么模拟这种情况呢?这就需要用到积分了。
现在我们假设雾气随高度变化的函数为,这里的f指的是雾气浓度fog density。从这个函数我们可以知道在高程为0时雾气浓度为1,在高程为100时,雾气浓度为0,图像大概下面所示,随着高程的增加雾浓度不断降低。
有了浓度随高程变化的公式,我们可以很轻松的算出不同高程的雾气浓度,例如高程50米雾气浓度为0.5。那么假设,现在我们从100米垂直看向地面0米的地方,那么这段距离的雾总量是多少呢,我们知道其实就是这个函数的线与坐标轴围成的三角形的面积,我们当然可以用 0.5 * 100 * 1.0 = 50 来得到最终的结果。
但是我们还有更科学的计算方式,那就是积分,雾气浓度随着高度变化的函数我们有了,积分上限为100,下限为0积分,公式为:
最终结果当然也是50,但是积分的存在可以帮我们计算曲线函数围成的面积。
2.公式推导
我们知道看向一个物体,最终物体被雾气遮挡的程度取决于我们视线上雾气的总和,如果我们能知道随着视线距离变化的雾气浓度就好了,那么之间对视线整个距离积分就可以得到雾气总和。但是我们知道高度雾的雾气浓度是随着高度变化而变化的,如果我们知道高度与视线距离的关系我们就其实也知道雾气浓度随距离变换的公式了。而距离与高度的公式就是 S = H / ρ。怎么就是这个了,这个ρ又是什么,看下图。
这个ρ就是归一化后的相机到点的向量在垂直于地面的向量(up)上的投影长度,并且如果与up方向相同(点乘大于0)就为正,方向相反就为复数。其实就是相机到点的向量在垂直于地面方向上的分量,有了这个分量我们就可以得到距离与高度的关系,也就是 S = H / ρ。
ps:按理来说数学上正确的公式应该是S = H / |ρ|,但是拥有了与方向相关的ρ可以帮我们不用纠结积分上下限。
有了这个 S = H / ρ 。那么我们就可以将浓度与高程的关系转为浓度与距离的关系。这时候我们对距离s进行积分,其实就是对 h/ρ 进行积分,而积分原本的上限S也当然变为了总高度H:
当然,上面公式积分是对整个雾气所在区域的积分,实际上的上限不是H,下限当然也不是0。如下图所示,积分的上下限总共有五种情况。
虽然情况多,但是实际上积分上限就是相机高度(cameraHeight)与雾气最高高度(fogMaxHeight)取最大值,下限就是目标点高度(pixelHeight)与雾气最高高度(fogMaxHeight)取最大值。因为ρ值与up反方向就为负数,我们得到的是沿着视线方向的雾浓度随距离的变换,这时候不用使积分上限一定要大于积分下限。所以积分就是从相机到目标距离的积分,转为对高度积分也就是相机高度到目标高度。
在shader中两行代码就可以确定。b为积分上限,a为积分下限。
float b = mix(cameraHeight, fogMaxHeight, step(fogMaxHeight, cameraHeight));
float a = mix(pixelHeight, fogMaxHeight, step(fogMaxHeight, pixelHeight));
三、shader代码
现在我们知道了求取视线上雾总量的公式,也知道了如何确定积分的上下限,接下来就是将公式转为shader代码了。
公式中的 f(h) 指的是雾浓度随高度变换的函数;ρ指的是相机到目标的向量归一化后在up向量上的投影,它代表视线长度与高度的关系;a,b为积分上下限。
1、获取ρ值
ρ我们知道是相机到目标的向量归一化后在up向量上的投影,相机到目标的向量很好获得,up向量其实也很好获得,它就是球心到相机的向量归一化后的结果。
cesium世界坐标中球心就是(0,0,0),相机的世界坐标有一个自动uniform可以获取,czm_viewerPositionWC,所以up向量为:
vec3 up = -1.0 * normalize(czm_viewerPositionWC);
而向量CP也很好获取,用 点的坐标 - 相机坐标 即可。这里有一个反直觉的,我们需要的是相机到目标的向量(positionToCamera),也就是CP,但是我们不能用C - P,而应该用P - C。
vec4 positionWC = getWorldCoordinate(depthTexture, v_textureCoordinates);
vec3 positionToCamera = vec3(vec3(positionWC) - czm_viewerPositionWC);
最后就是将positionToCamera归一化后投影到up上了,vh就是ρ。
// 得到a向量在b向量的投影长度,如果同向结果为正,异向结果为复
float projectVector(vec3 a, vec3 b) {
float scale = dot(a, b) / dot(b, b);
float k = scale / abs(scale);
return k * length(scale * b);
}
vec3 up = -1.0 * normalize(czm_viewerPositionWC);
float vh = projectVector(normalize(positionToCamera), up);
2、视线雾总量计算代码
再次将这个公式拿到,我们现在得到了前面的ρ值,还需要计算后面的积分,我们先使用简单的线性函数来描述雾浓度随高度的变化。随着高程的增加,雾浓度从1-0降低,在给定的雾高度后就小于等于0了。因为我们对积分上下限做了约束,上下限都不会超过fogH,所以不需要担心负数。
然后对其进行积分,积分上下限分别为b,a。
转为代码如下所示,globalDensity为全局浓度,它来自unifrom传递的全局浓度u_globalDensity/10,我们可以利用它来控制雾气整体浓度。
float fog = (b - a) - 0.5 * (pow(b, 2.0) - pow(a, 2.0)) / fogMaxHeight;
fog = globalDensity * fog / vh;
3.代码整合
我们将上面提到的代码整合在一起,当然还有上一篇文章讲过的一些辅助函数。
颜色混合是固定的格式,只要雾值在0-1之间就是这样。
fog = mix(0.0, 1.0, fog / (fog + 1.0))。这行代码就是为了将雾总量映射到0-1之间,因为视线上的雾总量并不是0-1之间的。
const fs = `
uniform sampler2D colorTexture; // 颜色纹理
uniform sampler2D depthTexture; // 深度纹理
in vec2 v_textureCoordinates; // 纹理坐标
uniform float u_earthRadiusOnCamera;
uniform float u_cameraHeight;
uniform float u_fogHeight;
uniform vec3 u_fogColor;
uniform float u_globalDensity;
// 通过深度纹理与纹理坐标得到世界坐标
vec4 getWorldCoordinate(sampler2D depthTexture, vec2 texCoords) {
float depthOrLogDepth = czm_unpackDepth(texture(depthTexture, texCoords));
vec4 eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, depthOrLogDepth);
eyeCoordinate = eyeCoordinate / eyeCoordinate.w;
vec4 worldCoordinate = czm_inverseView * eyeCoordinate;
worldCoordinate = worldCoordinate / worldCoordinate.w;
return worldCoordinate;
}
// 计算粗略的高程,依赖js传递的相机位置处的地球高程u_earthRadiusOnCamera。好处是计算量非常低
float getRoughHeight(vec4 worldCoordinate) {
float disToCenter = length(vec3(worldCoordinate));
return disToCenter - u_earthRadiusOnCamera;
}
// 得到a向量在b向量的投影长度,如果同向结果为正,异向结果为复
float projectVector(vec3 a, vec3 b) {
float scale = dot(a, b) / dot(b, b);
float k = scale / abs(scale);
return k * length(scale * b);
}
// 浓度积分高度雾
float linearHeightFog(vec3 positionToCamera, float cameraHeight, float pixelHeight, float fogMaxHeight) {
float globalDensity = u_globalDensity / 10.0;
vec3 up = -1.0 * normalize(czm_viewerPositionWC);
float vh = projectVector(normalize(positionToCamera), up);
float b = mix(cameraHeight, fogMaxHeight, step(fogMaxHeight, cameraHeight));
float a = mix(pixelHeight, fogMaxHeight, step(fogMaxHeight, pixelHeight));
float fog = (b - a) - 0.5 * (pow(b, 2.0) - pow(a, 2.0)) / fogMaxHeight;
fog = globalDensity * fog / vh;
fog = mix(0.0, 1.0, fog / (fog + 1.0));
return fog;
}
void main(void) {
vec4 color = texture(colorTexture, v_textureCoordinates);
vec4 positionWC = getWorldCoordinate(depthTexture, v_textureCoordinates);
float pixelHeight = getRoughHeight(positionWC);
vec3 positionToCamera = vec3(vec3(positionWC) - czm_viewerPositionWC);
float fog = linearHeightFog(positionToCamera, u_cameraHeight, pixelHeight, u_fogHeight);
out_FragColor = mix(color, vec4(u_fogColor, 1.0), fog);
}`
const customPostProcessStage = new PostProcessStage({
fragmentShader: fs,
uniforms: {
u_earthRadiusOnCamera: () => Cartesian3.magnitude(viewer.camera.positionWC) - viewer.camera.positionCartographic.height,
u_cameraHeight: () => viewer.camera.positionCartographic.height,
u_fogColor: () => new Color(0.8, 0.82, 0.84),
u_fogHeight: () => 1000,
u_globalDensity: () => 0.6,
}
})
viewer.scene.postProcessStages.add(customPostProcessStage)
初始化viewer,复制上面的代码,加载地形。没问题的话以及可以显示出下面的结果了。有问题的话记得检查一下是否开启了深度测试。
四、解决中间雾气缺失
在我们视角转到差不多平视的时候,我们会发现总是有一条线,因为如果目标点的高度和相机高度基本一致的话,积分上下限a,b也会接近相同,那么最终结果雾气值就为0了。但是我们知道这是错误的,我们用对高度积分代替了对距离的积分,但是在这个特殊的情况下,我们应该重新用回对距离的积分。并且因为线与高程接近平行,这时候的雾浓度在视线内是固定的,所以视线上的雾总量就等于这个高度的雾浓度*距离。
而怎么知道视线是否接近平行呢,就是看ρ值的情况,如果ρ小于某个数(我取的是 0.01),我们就认为它接近平行,并且这种情况需要在雾中才会进行处理。完善后的代码如下。
// 浓度积分高度雾
float linearHeightFog(vec3 positionToCamera, float cameraHeight, float pixelHeight, float fogMaxHeight) {
float globalDensity = u_globalDensity / 10.0;
vec3 up = -1.0 * normalize(czm_viewerPositionWC);
float vh = projectVector(normalize(positionToCamera), up);
float b = mix(cameraHeight, fogMaxHeight, step(fogMaxHeight, cameraHeight));
float a = mix(pixelHeight, fogMaxHeight, step(fogMaxHeight, pixelHeight));
float fog = (b - a) - 0.5 * (pow(b, 2.0) - pow(a, 2.0)) / fogMaxHeight;
fog = globalDensity * fog / vh;
if(abs(vh) <= 0.01 && cameraHeight < fogMaxHeight) {
float disToCamera = length(positionToCamera);
fog = globalDensity * (1.0 - cameraHeight / fogMaxHeight) * disToCamera;
}
fog = mix(0.0, 1.0, clamp(fog / (fog + 1.0), 0.0, 1.0));
return fog;
}
这时候同样是相机到目标向量接近与地面平行的情况下,雾气依然没有缺失,并且过渡完全不明显。
但是离的过近的话仍然会出现条带,也就是雾总量异常的情况。这种情况我也没有解决办法了,这应该还是因为webgl-float计算精度的问题,如果有解决办法或者其他的想法欢迎讨论一下。
五、添加雾气产生距离
1.错误做法
简单的将一定距离内的雾总量变为0是行不通的,这样会导致分界线特别明显割裂。比如下面的代码,如果点到相机的距离小于雾气产生距离,那么我们就让雾总量为0。
if(length(positionToCamera) < u_fogStartDis){
fog = 0.0;
}
结果就是下图,非常割裂。
2.正确做法
这需要分为两种情况,一种积分距离大于雾气产生距离,另一种则是小于。
当相机到点的距离大于雾气距离,那么也就是让相机沿着视线方向移动 雾气产生距离 的距离,如果相机距离小于雾气距离,那么就将相机移动到目标点。
由于积分需要的是相机高度,而消除条带的特殊计算需要的是距离,所以我们分别对两者进行计算。如下代码所示:
1、当点到到相机距离小于雾气产生距离,那么就是将相机移动到目标点,那么两者之间的向量就变为(0,0,0)了,而相机的高度也变为了目标的高度。
2、当点到到相机距离大于雾气产生距离,那么就是将相机沿着视线方向移动 u_fogStartDis * vh。
if(length(positionToCamera) < u_fogStartDis){
// 点到相机距离小于雾气产生距离
positionToCamera = vec3(0.0, 0.0, 0.0);
cameraHeight = pixelHeight;
}
else{
// 点到相机距离大于雾气产生距离
vec3 sub = normalize(positionToCamera) * u_fogStartDis;
positionToCamera = positionToCamera - sub;
cameraHeight -= u_fogStartDis * vh;
}
在webgl中我们尽量不使用条件语句,所以将上面的代码转变一下。
float s = step(u_fogStartDis, length(positionToCamera));
vec3 sub = mix(positionToCamera, normalize(positionToCamera) * u_fogStartDis, s);
positionToCamera -= sub;
cameraHeight = mix(pixelHeight, cameraHeight - u_fogStartDis * vh, s);
总体代码
// 线性浓度积分高度雾
float linearHeightFog(vec3 positionToCamera, float cameraHeight, float pixelHeight, float fogMaxHeight) {
float globalDensity = u_globalDensity / 10.0;
vec3 up = -1.0 * normalize(czm_viewerPositionWC);
float vh = projectVector(normalize(positionToCamera), up);
// 让相机沿着视线方向移动 雾气产生距离 的距离
float s = step(u_fogStartDis, length(positionToCamera));
vec3 sub = mix(positionToCamera, normalize(positionToCamera) * u_fogStartDis, s);
positionToCamera -= sub;
cameraHeight = mix(pixelHeight, cameraHeight - u_fogStartDis * vh, s);
float b = mix(cameraHeight, fogMaxHeight, step(fogMaxHeight, cameraHeight));
float a = mix(pixelHeight, fogMaxHeight, step(fogMaxHeight, pixelHeight));
float fog = (b - a) - 0.5 * (pow(b, 2.0) - pow(a, 2.0)) / fogMaxHeight;
fog = globalDensity * fog / vh;
if(abs(vh) <= 0.01 && cameraHeight < fogMaxHeight) {
float disToCamera = length(positionToCamera);
fog = globalDensity * (1.0 - cameraHeight / fogMaxHeight) * disToCamera;
}
fog = mix(0.0, 1.0, fog / (fog + 1.0));
return fog;
}
结果,可以看到现在就有过渡效果了,不再那么割裂。
六、总结
通过对视线上的雾气浓度进行积分得到视线上的雾气总量,这比较真实的反映了高度雾的效果,既有高度上的变化也有距离上的变化。
本文主要讲的是线性浓度积分高度雾,如果想实现指数浓度积分高度雾也非常的简单,只需要将雾浓度随高度变化的函数换成指数函数就可以了。
例如换成。a为全局浓度,b为浓度衰减系数。然后最终代码如下所示:
// 指数浓度积分高度雾
float exponentialHeightFog(vec3 positionToCamera, float cameraHeight, float pixelHeight, float fogMaxHeight) {
float globalDensity = u_globalDensity / 10.0;
vec3 up = -1.0 * normalize(czm_viewerPositionWC);
float vh = projectVector(normalize(positionToCamera), up);
// 让相机沿着视线方向移动 雾气产生距离 的距离
float s = step(u_fogStartDis, length(positionToCamera));
vec3 sub = mix(positionToCamera, normalize(positionToCamera) * u_fogStartDis, s);
positionToCamera -= sub;
cameraHeight = mix(pixelHeight, cameraHeight - u_fogStartDis * vh, s);
float b = mix(cameraHeight, fogMaxHeight, step(fogMaxHeight, cameraHeight));
float a = mix(pixelHeight, fogMaxHeight, step(fogMaxHeight, pixelHeight));
float k = (-globalDensity / u_heightFalloff);
float fog = k / vh * (exp(-u_heightFalloff * b) - exp(-u_heightFalloff * a));
if(abs(vh) <= 0.003 && cameraHeight < fogMaxHeight) {
float disToCamera = length(positionToCamera);
fog = globalDensity * exp(-u_heightFalloff * cameraHeight) * disToCamera;
}
fog = mix(0.0, 1.0, fog / (fog + 1.0));
return fog;
}
标签:积分,float,雾气,后处理,vec3,cesium,fogMaxHeight,positionToCamera
From: https://blog.csdn.net/weixin_70945905/article/details/143368883