0、前言
本方法是基于 在低性能设备上在近距离区域渲染大面积可变更云/雾 所设想的一种解决思路,本身存在一定的局限性,若追求更高的渲染质量还是建议使用诸如光线追踪(RayTrace)等算法进行渲染。
为何该方法是“近处的”,因为当前使用的处理方案是一个基于放大、模糊的后处理着色器,模拟云雾在屏幕前的绘制结果。由于采用的模糊着色器在屏幕空间中对小面积图元——即远处的物体——进行处理时效果远不如大型面积图元的效果,故本文中将其称为“近处的”。
由于该方法是在TeaCon期间为实现对应图形绘制所设计,故本文中所有运行代码的运行皆基于Java-Minecraft-Neoforge框架之下,但着色器与流程部分理论上可在其他环境下运行。
本文中所有渲染测试与结果均基于下述运行环境:
- Java:jbr_jcef-21.0.3-windows-x64-b509.11
- LWJGL/OpenGL:lwjgl-opengl-3.3.3
- Minecraft:1.21
- NeoForge:21.0.160
关联硬件信息:
- CPU:13th Gen Intel(R) Core(TM) i7-1360P
- GPU:Intel(R) Iris(R) Xe Graphics
效果预览:
1、原理
该方法通过动态生成网格,通过顶点着色器进行简单的顶点处理,然后将物体绘制至指定的FBO中,通过高斯模糊等后处理着色器进行处理,最后将其重新绘制至输出FBO(或主FBO)中,从而实现单个周期的渲染流程。
2、网格生成
2.1、为何需要动态生成网格
首先,动态生成网格并非刚需,自行实现时仍可采用使用已有模型的网格(例如一个按照一定单位细分过的cube)进行绘制。但文中所需场景为:允许用户通过可视化图形界面对云/雾进行增删改的操作,故需要在每次信息变更后重新生成网格。
2.2、生成网格
在本文中,网格将按照最大长宽高不超过2的立方体格式进行生成,以下是用与存储未待生成网格的数据结构:
public record CloudData(
Vector3f position,
Vector3f size,
int cullFlag
) {}
在生成网格前,仍需要对该数据进行分割,以确保添加到网格中的每一个CloudData都是小于等于2,并剔除掉多余的面或立方体:
public void split(List<CloudData> listIn) {
int xChunks = (int) Math.ceil(size.x / (float) CloudRenderer.SINGLE_CLOUD_SIZE);
int yChunks = (int) Math.ceil(size.y / (float) CloudRenderer.SINGLE_CLOUD_SIZE);
int zChunks = (int) Math.ceil(size.z / (float) CloudRenderer.SINGLE_CLOUD_SIZE);
for (int i = 0; i < xChunks; i++) {
for (int j = 0; j < yChunks; j++) {
for (int k = 0; k < zChunks; k++) {
int flag = this.cullFlag;
if (i < xChunks - 1) {
flag |= CloudRenderer.CULL_XP;
}
if (i != 0) {
flag |= CloudRenderer.CULL_XN;
}
if (j < yChunks - 1) {
flag |= CloudRenderer.CULL_YP;
}
if (j != 0) {
flag |= CloudRenderer.CULL_YN;
}
if (k < zChunks - 1) {
flag |= CloudRenderer.CULL_ZP;
}
if (k != 0) {
flag |= CloudRenderer.CULL_ZN;
}
// 如果该Cube完全不可见,则直接跳过
if (flag == CloudRenderer.INVISIBLE) {
continue;
}
Vector3f position = new Vector3f(
this.position.x + i * CloudRenderer.SINGLE_CLOUD_SIZE,
this.position.y + j * CloudRenderer.SINGLE_CLOUD_SIZE,
this.position.z + k * CloudRenderer.SINGLE_CLOUD_SIZE
);
Vector3f size = new Vector3f(
Math.min(CloudRenderer.SINGLE_CLOUD_SIZE, this.size.x - i * CloudRenderer.SINGLE_CLOUD_SIZE),
Math.min(CloudRenderer.SINGLE_CLOUD_SIZE, this.size.y - j * CloudRenderer.SINGLE_CLOUD_SIZE),
Math.min(CloudRenderer.SINGLE_CLOUD_SIZE, this.size.z - k * CloudRenderer.SINGLE_CLOUD_SIZE)
);
listIn.add(new CloudData(position, size, flag));
}
}
}
}
将刚才的网格数据进行缓存后,使用 com.mojang.blaze3d.vertex.BufferBuilder 依次处理缓存数据,将其添加面至网格中,最后将其绑定置VBO中。
3、网格绘制
在将VBO中的定点信息提交渲染前,由于渲染目标是需要一个仅绘制了该VBO中顶点图像的FBO,在此之前,需要对目标的FBO进行一些处理,此处演示均使用 com.mojang.blaze3d.pipeline.RenderTarget 作为FBO的Java接口类:将需要渲染的FBO清空,并将主FBO中的深度缓冲帧复制至该FBO中,这样可以使得有一个干净的FBO,且渲染时同步主FBO中的深度缓冲帧。
RenderTarget finalTarget = this.getFinalTarget();
RenderTarget mainRenderTarget = Minecraft.getInstance().getMainRenderTarget();
final int width = finalTarget.viewWidth;
final int height = finalTarget.viewHeight;
finalTarget.clear(Minecraft.ON_OSX);
// 从 mainRenderTarget 复制深度缓冲帧至 finalTarget
GL30C.glBindFramebuffer(GL30C.GL_READ_FRAMEBUFFER, mainRenderTarget.frameBufferId);
GL30C.glBindFramebuffer(GL30C.GL_DRAW_FRAMEBUFFER,finalTarget.frameBufferId);
GL30C.glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL30C.GL_DEPTH_BUFFER_BIT, GL30C.GL_NEAREST);
GL30C.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, 0);
将缓冲帧复制完成后,将该FBO绑定,并将先前的VBO使用指定的着色器提交渲染。此处使用的顶点着色器通过将坐标和偏移值传入一个SimpleX2D噪声生成函数,计算出一个随着时间变化的变化的沿着y轴顶点偏移(左手坐标系),且将计算出的值与输出颜色相乘,模拟一个深度颜色。其中:函数minecraft_mix_light是Minecraft提供的一个光照顶点计算函数,在非Minecraft环境下可替换为自己的的函数。
#version 150
in vec3 Position;
in vec4 Color;
in vec3 Normal;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
uniform vec3 ChunkOffset;
uniform float GameTime;
uniform vec3 LightDirection0;
uniform vec3 LightDirection1;
out vec4 vertexColor;
void main() {
vec3 position = Position;
float delta = GameTime * 943 + (Position.x + Position.y + Position.z);
float wave = simpleX2D(Position.xz + vec2(delta / 10));
position.y += wave / 2;
gl_Position = ProjMat * ModelViewMat * vec4(position + ChunkOffset, 1.0);
vertexColor = minecraft_mix_light(LightDirection0, LightDirection1, Normal, Color);
vertexColor.rgb *= wave;
}
本文使用的片段着色器仅为一个简单的颜色混合,此处变不多赘述。以预览图的场景为蓝本,将当前FBO中的图像混合输出到主FBO中,渲染出的结果如下:
4、后处理
后处理将使用 net.minecraft.client.renderer.PostChain 对图像进行处理,故此处不会对细节部分(如FBO间的传递)进行过多赘述。
首先,通过将绘制了图像直接出至屏幕,会得到以下图像:
对图像的处理步骤如下:
- 在x、y轴方向上分别对图像中非0区域进行增幅,使得图像中带颜色的区域占比更高
- 对增幅后的图像进行模糊处理
- 对的图像使用高斯模糊进行处理,获得一个更大的模糊区域
4.1、增幅(Amplify)
由于增幅后仍需要对图像进行模糊处理,所以在放大使用的着色器中:采样次数可以酌情减少,采样步长可以酌情增加。本文中采样步长使用5.43作为样例,采样次数为两方向各采样两次:
void main() {
vec4 blurred = texture(DiffuseSampler, texCoord);
vec2 step = sampleStep * 5.43;
blurred = max(texture(DiffuseSampler, texCoord + step), blurred);
blurred = max(texture(DiffuseSampler, texCoord + step * 5.2), blurred);
blurred = max(texture(DiffuseSampler, texCoord - step), blurred);
blurred = max(texture(DiffuseSampler, texCoord - step * 5.2), blurred);
fragColor = blurred;
}
经过增幅后的图像如下:
4.2、模糊
对增幅后的图像添加模糊效果,可以使得使用高斯模糊后得到一个更平滑的过渡。此处使用的模糊着色器为Minecraft提供的box_blur,若在非Minecraft环境下可参考下述着色器:
void main() {
vec4 blurred = vec4(0.0);
float actualRadius = round(Radius * RadiusMultiplier);
for (float a = -actualRadius + 0.5; a <= actualRadius; a += 2.0) {
blurred += texture(DiffuseSampler, texCoord + sampleStep * a);
}
blurred += texture(DiffuseSampler, texCoord + sampleStep * actualRadius) / 2.0;
fragColor = blurred / (actualRadius + 0.5);
}
4.3、高斯模糊
本文中使用的高斯模糊的模糊核大小为13,采样步长为9.43,在设备性能允许的情况下可酌情缩短步长或添加采样次数:
const float weight[] = float[] (
0.0896631113333857,
0.0874493212267511,
0.0811305381519717,
0.0715974486241365,
0.0601029809166942,
0.0479932050577658,
0.0364543006660986,
0.0263392293891488,
0.0181026699707781,
0.0118349786570722,
0.0073599963704157,
0.0043538453346397,
0.0024499299678342
);
void main() {
vec4 blurred = vec4(0.0);
vec2 step = sampleStep * 4.32;
for (int i = 1; i < 13; i++) {
blurred += texture(DiffuseSampler, texCoord + step * i) * weight[i];
blurred += texture(DiffuseSampler, texCoord - step * i) * weight[i];
}
fragColor = blurred * 1.1f;
}
处理后图像如下:
最后将处理好的内容与主FBO混合,则可得到开头放置的图像。
5、附录
该方法基于算法问题,仍存在诸如:错误覆盖、远处模糊等问题,相较于绘制位于在世界坐标中固定的物体,更适合绘制在镜头前的雾等效果。关于对错误覆盖的问题,当前设想是通过绘制前后的深度缓冲帧,获取可将云雾覆盖物体的遮罩,在最后将多余的区域进行剔除。
关联内容代码仓库:GitHub:ThatSkyInteractions