Translation notice
This page was translated with machine translation and may contain inaccuracies. If you can help improve it, please open an issue or submit a pull request.


Shader Practice: Code Rain Production
Preface
This is a relatively short tutorial, which is part of the shader practice chapter. It mainly uses code rain as an example to introduce the following skills and technologies:
Marking models (faces) by textures, the idea of storing more data in color channels
Use of coordinate sampling from screen
Calculate the mapping relationship from the submap coordinate to the sprite map coordinate, thereby accessing and only accessing the entire texture on the surface.
Programmatic drawing code rain effect
Effect display

1. Mark the model (surface) by texture
In this example, we only modified a specific model to display the code rain effect on its surface. In order to achieve this, we need to store more data in the color channel so that these models can be recognized in the fragment shader.
The texture we read is RGBA four-channel data, and only one channel is used to store the shape of the character. The other three channels are free to use, so we can use one of the color channels (or three together) for marking. For example, in this example, the opacity is set to
Although solid render objects cannot output transparency channels, they can still be opaque.
When detecting, we usetextureLod()function to read the color channel data and determine whether the opacity is equal to
bool roughly_equals(float a, float b) { // 由于浮点数精度问题,不适合直接使用等号判断
return abs(a - b) < 0.000001;
}
vec4 color = textureLod(Sampler0, texCoord0, 0.0); // 将Lod等级设为0,避免Mipmap带来的误差
if (roughly_equals(color.a, 254.0 / 255.0)) {
// 这里是代码雨效果的实现
}2. Use of coordinate sampling from the screen
We want the code rain effect to be sampled directly with screen coordinates like the end portal, so we need to useprojection_from_position()function gets the screen coordinate.
// 该函数在 projection.glsl 中定义
vec4 projection_from_position(vec4 position) {
vec4 projection = position * 0.5; // [-w, w] -> [-w/2, w/2]
projection.xy = vec2(projection.x + projection.w, projection.y + projection.w); // for xy: [-w/2, w/2] -> [0, w]
projection.zw = position.zw; // for zw: no change
return projection;
}The input to this function is the position of the fragment in clipping space (i.e.gl_Position), in previous tutorials we have mentioned the position of the clipping space many times, and the coordinate of the final retained vertex in the clipping space isprojection_from_position()After function, the xy component is mapped totextureProj()function to automatically complete perspective division and sampling.
This function accepts two parameters, the first parameter is the sampler and the second parameter is avec4coordinate vector of type. This function automatically performs perspective division on the first three components of the coordinate vector, and then uses the first two components as texture coordinates for sampling.
vec4 ndc = projection_from_position(gl_Position);
vec4 screenColor = textureProj(Sampler0, ndc);but! This is the logic used by Mojang in end door rendering, but we can actually directly use GLSL's built-in special variables and global variables to obtain the normalized screen coordinates, that is
vec2 ScreenPos = gl_FragCoord.xy / ScreenSize;
vec4 screenColor = texture(Sampler0, ScreenPos);The advantage of the former method is that you can directly use the z component after perspective division for depth testing (such as the end door block), while the latter method is more concise and efficient (but you need to obtain the screen size from globalsScreenSize, check whether the version you are using supports this global variable). We use the latter approach in the code below.
3. Calculate the mapping relationship from subgraph coordinates to sprite map coordinates
Since Minecraft's baked models are composed of voxels, Minecraft renders each quadrilateral face (2 triangles, sharing 4 vertices) in order. So, GLSL’s built-in variablesgl_VertexID % 4The result is that it cycles from 0 to 3 as each quad face is rendered.
So we can usegl_VertexID % 4To calculate the subgraph coordinate (NormalizedUV) of each vertex within the quadrilateral surface, that is:
if (gl_VertexID % 4 == 0) {
NormalizedUV = vec2(0.0, 1.0);
} else if (gl_VertexID % 4 == 1) {
NormalizedUV = vec2(0.0, 0.0);
} else if (gl_VertexID % 4 == 2) {
NormalizedUV = vec2(1.0, 0.0);
} else {
NormalizedUV = vec2(1.0, 1.0);
}Now we hope to find the mapping relationship from sub-picture coordinates to sprite map coordinates. Assuming there is no rotation or inversion between the sub-picture area and the sprite map area, then we have the following relationship:
Then we need to find the vector
Obviously, under the pipeline limitations of Minecraft, we cannot obtain enough information in vsh. Therefore, we need to use the partial derivative function in fsh to solve, that is, use the rate of change of the known variable on the screen coordinate to solve the unknown quantity in the above equation.
In fact,
To avoid notational confusion, we will
Correct the above two equations respectively
Similarly, for the above two equations, respectively
You can choose either of the two expression methods. Here we choose
Since the partial derivative function and division in GLSL are component-wise operations, we can directly write:
Note: operator
i.e. function dFdx(), represents the screen coordinateFind the partial derivative. In the same way, we have i.e. function dFdy().
Next we find the vector
Therefore we can directly write:
The following is the code implementation:
vec2 k = dFdx(texCoord0) / dFdx(NormalizedUV);
vec2 b = texCoord0 - k * NormalizedUV;
vec2 SpriteUV = k * SpriteNormalizedUV + b; // 这一行在最终采样时使用,绘制代码中我们假设在子图内采样即可Note that the correctness of the above formula depends on there being no rotation or inversion between the submap area and the sprite area. If there is rotation or inversion, the above method will not be able to calculate the mapping relationship correctly. Therefore, the code for setting subgraph coordinates in vsh must ensure a specific order, and Mojang has a precedent of modifying the vertex order between different versions, so please be sure to test and verify it in actual use.
4. Draw code rain
Borrowed here Shadertoy, Matrix Code (by _polymath) code implementation, with some modifications to adapt to the Minecraft shader environment.
#define CELLS vec2(128.0,60.0)
#define FALLERS 7.0
#define FALLERHEIGHT 12.0
vec2 rand(vec2 uv) {
vec2 r = floor(abs(mod(cos(
uv * 652.6345 + uv.yx * 534.375 +
GameTime * 1200 * 0.0000005 * dot(uv, vec2(0.364, 0.934))),
0.001)) * 16000.0);
return mod(r, 16.0);
}
float fallerSpeed(float col, float faller) {
return mod(cos(col * 363.435 + faller * 234.323), 0.1) * 1.0 + 0.3;
}// 在我们的 if 分支中
vec2 k = dFdx(texCoord0) / dFdx(NormalizedUV);
vec2 b = texCoord0 - k * NormalizedUV;
vec2 uv = gl_FragCoord.xy / ScreenSize;
vec2 pix = mod(uv, 1.0/CELLS);
vec2 cell = (uv - pix) * CELLS;
pix *= CELLS * vec2(0.8, 1.0) + vec2(0.1, 0.0);
float c = texture(Sampler0, k * (rand(cell) + pix) / 16.0 + b).x;
float brightness = 0.0;
for (float i = 0.0; i < FALLERS; ++i) {
float f = 3.0 - cell.y * 0.05 -
mod((GameTime * 1200 + i * 3534.34) * fallerSpeed(cell.x, i), FALLERHEIGHT);
if (f > 0.0 && f < 1.0)
brightness += f;
}
fragColor = vec4(0.0, c * brightness, 0.0, 1.0);Let’s analyze the drawing principle line by line:
First forrandfunction, which accepts avec2typeuvcoordinate, and returns avec2Type of pseudo-random number, since cell takes a series of integers, sorand(cell)will return a
Next isfallerSpeedfunction, which accepts two floating point parameterscolandfaller, and returns a speed value of floating point type. This function also uses the periodicity of the trigonometric function to generate the speed value, so that the falling speed of the code rain in each column is slightly different.
In the main drawing code, CELLS defines the number of columns and rows of the code rain.pixThe calculation result is the offset coordinate of the current fragment within a cell,cellThe calculation result is the coordinate of the cell where the current fragment is located.
pix *= CELLS * vec2(0.8, 1.0) + vec2(0.1, 0.0);What this line of code does ispixZoom and pan so that the characters of Code Rain have a certain margin within the cell, thereby preventing the characters from sticking to the edge of the cell. ultimately a
The coordinates for sampling below are(rand(cell) + pix) / 16.0,inrand(cell)A pseudo-random offset based on the cell coordinate is generated. This offset is used to select random characters in the subgraph, andpixIt is the offset of the current fragment within the cell. Add the two and divide by 16.0 because the characters in the subgraph are arranged in a 16x16 grid.
Combined with the transformation formula mentioned above, we will(rand(cell) + pix) / 16.0Substitute intoSpriteNormalizedUV, the final sampling coordinate isk * (rand(cell) + pix) / 16.0 + b。
Sample from there, take the first channel to get the character shapecFinally, we need to calculate the brightness of the fragmentbrightness. The calculation of brightness is implemented through a loop, and the number of loops isFALLERS, that is, the number of drop columns of code rain. This achieves the effect of the tail becoming darker and the head becoming brighter during the falling process.
5. Final procedures
Finally, we combine the various parts and get the complete fragment shader code as follows:
// terrain.vsh
#version 330
#moj_import <minecraft:fog.glsl>
#moj_import <minecraft:globals.glsl>
#moj_import <minecraft:chunksection.glsl>
#moj_import <minecraft:projection.glsl>
in vec3 Position;
in vec4 Color;
in vec2 UV0;
in ivec2 UV2;
in vec3 Normal;
uniform sampler2D Sampler2;
out float sphericalVertexDistance;
out float cylindricalVertexDistance;
out vec4 vertexColor;
out vec2 texCoord0;
out vec2 NormalizedUV; // 这里增加了一个输出变量
vec4 minecraft_sample_lightmap(sampler2D lightMap, ivec2 uv) {
return texture(lightMap, clamp((uv / 256.0) + 0.5 / 16.0, vec2(0.5 / 16.0), vec2(15.5 / 16.0)));
}
void main() {
vec3 pos = Position + (ChunkPosition - CameraBlockPos) + CameraOffset;
gl_Position = ProjMat * ModelViewMat * vec4(pos, 1.0);
sphericalVertexDistance = fog_spherical_distance(pos);
cylindricalVertexDistance = fog_cylindrical_distance(pos);
vertexColor = Color * minecraft_sample_lightmap(Sampler2, UV2);
texCoord0 = UV0;
if (gl_VertexID % 4 == 0) { // 通过顶点ID计算子图坐标,注意顺序
NormalizedUV = vec2(0.0, 1.0);
} else if (gl_VertexID % 4 == 1) {
NormalizedUV = vec2(0.0, 0.0);
} else if (gl_VertexID % 4 == 2) {
NormalizedUV = vec2(1.0, 0.0);
} else {
NormalizedUV = vec2(1.0, 1.0);
}
}// terrain.fsh
#version 330
#moj_import <minecraft:fog.glsl>
#moj_import <minecraft:globals.glsl>
#moj_import <minecraft:chunksection.glsl>
uniform sampler2D Sampler0;
in float sphericalVertexDistance;
in float cylindricalVertexDistance;
in vec4 vertexColor;
in vec2 texCoord0;
in vec2 NormalizedUV; // 这里增加了一个输入变量
out vec4 fragColor;
// 这里的代码是新版原本渲染增加的采样函数,可能看起来比较陌生,我们不需要深入理解它的实现细节
vec4 sampleNearest(sampler2D sampler, vec2 uv, vec2 pixelSize, vec2 du, vec2 dv, vec2 texelScreenSize) {
...
}
vec4 sampleNearest(sampler2D source, vec2 uv, vec2 pixelSize) {
...
}
// Rotated Grid Super-Sampling
vec4 sampleRGSS(sampler2D source, vec2 uv, vec2 pixelSize) {
...
}
bool roughly_equals(float a, float b) { // 由于浮点数精度问题,不适合直接使用等号判断
return abs(a - b) < 0.000001;
}
#define CELLS vec2(128.0,60.0) // 这一项配置了代码雨的列数和行数
#define FALLERS 4.0 // 决定在同一列上同时模拟的下落雨滴数量。值越大,同一列上同时出现的雨滴流越多,显得越密集
#define FALLERHEIGHT 16.0 // 控制单个雨滴的视觉长度和下落循环的周期。值越大,雨滴越长,且两次重置之间的时间间隔也越长
vec2 rand(vec2 uv) { // 基于三角函数的伪随机数生成函数
vec2 r = floor(abs(mod(cos(
uv * 652.6345 + uv.yx * 534.375 +
GameTime * 1200 * 0.0000005 * dot(uv, vec2(0.364, 0.934))),
0.001)) * 16000.0);
return mod(r, 16.0);
}
float fallerSpeed(float col, float faller) { // 基于三角函数的速度生成函数
return mod(cos(col * 363.435 + faller * 234.323), 0.1) * 1.0 + 0.3;
}
void main() {
vec4 raw_color = textureLod(Sampler0, texCoord0, 0.0);
if (roughly_equals(raw_color.a, 254.0 / 255.0)) {
vec2 k = dFdx(texCoord0) / dFdx(NormalizedUV);
vec2 b = texCoord0 - k * NormalizedUV;
vec2 uv = gl_FragCoord.xy / ScreenSize;
vec2 pix = mod(uv, 1.0/CELLS);
vec2 cell = (uv - pix) * CELLS;
pix *= CELLS * vec2(0.8, 1.0) + vec2(0.1, 0.0);
float c = texture(Sampler0, k * (rand(cell) + pix) / 16.0 + b).x;
float brightness = 0.0;
for (float i = 0.0; i < FALLERS; ++i) {
float f = 6.0 - cell.y * 0.05 -
mod((GameTime * 1200 + i * 3534.34) * fallerSpeed(cell.x, i), FALLERHEIGHT);
if (f > 0.0 && f < 1.0)
brightness += f;
}
fragColor = vec4(0.0, c * brightness, 0.0, 1.0);
}else{
// 原版渲染分支
vec4 color = (UseRgss == 1 ? sampleRGSS(Sampler0, texCoord0, 1.0f / TextureSize) : sampleNearest(Sampler0, texCoord0, 1.0f / TextureSize)) * vertexColor;
color = mix(FogColor * vec4(1, 1, 1, color.a), color, ChunkVisibility);
#ifdef ALPHA_CUTOUT
if (color.a < ALPHA_CUTOUT) {
discard;
}
#endif
fragColor = apply_fog(color, sphericalVertexDistance, cylindricalVertexDistance, FogEnvironmentalStart, FogEnvironmentalEnd, FogRenderDistanceStart, FogRenderDistanceEnd, FogColor);
}
}The texture is through vanillaresource packtextures/font/asciiModified, here we use the r channel as the shape channel, and the g and b channels are set to

Apply the texture to the block to get the final effect:

What is modified here is the terrainshader, so the effect only applies to blocks placed in the world (non-entity types). If you want the code rain effect to be applied to entity type models, you can transplant the corresponding code to
entity.vshandentity.fshmiddle. Special note is that if you want to apply particle effects, you need to pay attention to the problem of z-flighting. The particles may have 24 vertices, but the rendering logic of Minecraft is to render all the faces in one direction, and then render all the faces in the next direction, so it cannot passgl_VertexID % 24To adapt, readers are welcome to propose better solutions. (However, it is very convenient to use display entities to simulate particles)