一种基于后处理的实时近处云/雾体渲染实现设想与方法

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间的传递)进行过多赘述。

首先,通过将绘制了图像直接出至屏幕,会得到以下图像:

对图像的处理步骤如下:

  1. 在x、y轴方向上分别对图像中非0区域进行增幅,使得图像中带颜色的区域占比更高
  2. 对增幅后的图像进行模糊处理
  3. 对的图像使用高斯模糊进行处理,获得一个更大的模糊区域

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值