Skip to content

Commit 681cff7

Browse files
committed
Tutorial 3 - Advanced Lighting has been added
1 parent 4d4e9bc commit 681cff7

21 files changed

+453
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This repository contains information and tutorials on how to write custom shaders for Minecraft use the Optifine mod. The tutorials start at the "Introduction" folder and then the "Tutorial N - ABCD" folders contain information on how to add a certain effect to your shader. The "Introduction" folder is not a shader pack, but the "Tutorial N - ABCD" is. For these tutorials the only requirement is a basic knowledge of graphics programming (you need to know everything up to the concept of rendering to a texture), although I plan to add another tutorial that explains all the basics in detail.
44

5+
I originally created this project because when I was learning how to write Minecraft shaders, there was little to no information I could find on how to write them. These set of tutorials aims to get rid of that issue for those who want to learn how to write a Minecraft shader.
6+
7+
If you want to contribute your own tutorial, create a pull request. One of my end goals with this project is the turn this into a community effort.
8+
59
## How to download
610

711
First make sure you have Optifine installed and find your `shaderpacks` folder.

Tutorial 2 - Composite and GBuffers/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,4 @@ void main(){
224224

225225
![Simple Diffuse Lighting](images/simple_diffuse.png)
226226

227-
Notice how blocks facing away from the sun are lit less that those that are. Also notice how the sky looks completely broken. This is because `composite` is a fullscreen pass and that doesn't mean it does not run for the sky as well. We will fix this bug in a later tutorial. In the next tutorial we will look at using the lightmap to account for both torch and sky lighting.
227+
Notice how blocks facing away from the sun are lit less that those that are. Also notice how the sky looks completely broken. This is because `composite` is a fullscreen pass and that doesn't mean it does not run for the sky as well. We will fix this bug in a later tutorial. In the next tutorial we will look at using the lightmap to account for both torch and sky lighting, and we will add shadows to our shaders.
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# Tutorial 3
2+
3+
In this tutorial we will look at implementing the lightmap and shadows.
4+
5+
## The lightmap
6+
7+
In the gbuffers vertex shader, the lightmap is accesible by using `gl_MultiTexCoord1`. The `s` channel is the torch lightmap, and the `t` channel is the sky lightmap..However this value is not in the range [0, 1]. Instead it is in the range [0, 15], but that can change depending on the version of Minecraft you are playing on. Thankfully, if we multiply our lightmap by `gl_TextureMatrix[1]` to move it into the range [0, 1]. Well actually, not quite that range. It gets moved into the range [1.05 / 32, 32/33.05]. We can do some quick math to get it into the range [0, 1]. We have to also declare a new varying variable, `LightmapCoords`, to move the lightmap values from the vertex shader to the fragment shader. It looks something like this:
8+
9+
```glsl
10+
varying vec2 LightmapCoords;
11+
12+
[...]
13+
14+
void main() {
15+
[...]
16+
// Use the texture matrix instead of dividing by 15 to maintain compatiblity for each version of Minecraft
17+
LightmapCoords = mat2(gl_TextureMatrix[1]) * gl_MultiTexCoord1.st;
18+
// Transform them into the [0, 1] range
19+
LightmapCoords = (LightmapCoords * 33.05f / 32.0f) - (1.05f / 32.0f);
20+
[...]
21+
}
22+
```
23+
24+
And in the fragment shader, we have to write the values to a color texture:
25+
26+
```glsl
27+
varying vec2 LightmapCoords;
28+
[...]
29+
30+
void main() {
31+
[...]
32+
/* DRAWBUFFERS:012 */
33+
// Write the values to the color textures
34+
gl_FragData[0] = albedo;
35+
gl_FragData[1] = vec4(Normal * 0.5f + 0.5f, 1.0f);
36+
gl_FragData[2] = vec4(LightmapCoords, 0.0f, 1.0f);
37+
}
38+
39+
```
40+
41+
Then in compsite we read back the lightmap values like so:
42+
43+
```glsl
44+
vec2 Lightmap = texture2D(colortex2, TexCoords).rg;
45+
```
46+
47+
Remember, we are now using a new color texture, `colortex2`. We have to set it's format and declare it. I use `RGB16` for the format since we don't need the alpha channel. Also a small change from the last tutorial: I made a mistake and set the normal color texture format to `RGBA16`. This is wrong since we don't use the alpha channel, so it should instead, like the lightmap color texture, be `RGB16`. The reason why I keep the blue channel in the lightmap instead of using `RG16` is because in the next upcoming tutorials, we will make use of that space to store material masks and whatnot.
48+
49+
If we visualise the lightmap, we see something like this:
50+
51+
![Lightmap visualised](images/torch_lightmap.png)
52+
![Lightmap visualised](images/sky_lightmap.png)
53+
54+
As you can see, in the first screenshot, while looking at glowstone, the lightmap looks more redder since there is a higher torch lighting value there. In the second screenshot, the part under the overhang thing looks darker since there is not torch light map and those blocks have less exposure to the sky. This may look good, but the rate at which the lightmap attenuates is not very realistic. We will have to modify the lightmap with out own functions. I will be using these:
55+
56+
```glsl
57+
float AdjustLightmapTorch(in float torch) {
58+
const float K = 2.0f;
59+
const float P = 5.06f;
60+
return K * pow(torch, P);
61+
}
62+
63+
float AdjustLightmapSky(in float sky){
64+
float sky_2 = sky * sky;
65+
return sky_2 * sky_2;
66+
}
67+
68+
vec2 AdjustLightmap(in vec2 Lightmap){
69+
vec2 NewLightMap;
70+
NewLightMap.x = AdjustLightmapTorch(Lightmap.x);
71+
NewLightMap.y = AdjustLightmapSky(Lightmap.y);
72+
return NewLightMap;
73+
}
74+
```
75+
76+
And this time we get much better results:
77+
78+
![Lightmap visualised with better attenuation](images/better_torch_lightmap.png)
79+
![Lightmap visualised with better attenuation](images/better_sky_lightmap.png)
80+
81+
Now we have to get the color of the lighting from the lightmap value. I use this function to do so:
82+
83+
```glsl
84+
// Input is not adjusted lightmap coordinates
85+
vec3 GetLightmapColor(in vec2 Lightmap){
86+
// First adjust the lightmap
87+
Lightmap = AdjustLightmap(Lightmap);
88+
// Color of the torch and sky. The sky color changes depending on time of day but I will ignore that for simplicity
89+
const vec3 TorchColor = vec3(1.0f, 0.25f, 0.08f);
90+
const vec3 SkyColor = vec3(0.05f, 0.15f, 0.3f);
91+
// Multiply each part of the light map with it's color
92+
vec3 TorchLighting = Lightmap.x * TorchColor;
93+
vec3 SkyLighting = Lightmap.y * SkyColor;
94+
// Add the lighting togther to get the total contribution of the lightmap the final color.
95+
vec3 LightmapLighting = TorchLighting + SkyLighting;
96+
// Return the value
97+
return LightmapLighting;
98+
}
99+
```
100+
101+
Then in `main` we get the lightmap color and use that in lighting calculations:
102+
103+
```glsl
104+
vec2 Lightmap = texture2D(colortex2, TexCoords).rg;
105+
// Get the lightmap color
106+
vec3 LightmapColor = GetLightmapColor(Lightmap);
107+
// Compute cos theta between the normal and sun directions
108+
float NdotL = max(dot(Normal, normalize(sunPosition)), 0.0f);
109+
// Do the lighting calculations
110+
vec3 Diffuse = Albedo * (LightmapColor + NdotL + Ambient);
111+
```
112+
113+
If we reload our shader with F3+R we get something that looks like this:
114+
115+
![Lightmap demo](images/lightmap_demo_torch.png)
116+
![Lightmap demo](images/lightmap_demo_sky.png)
117+
118+
## Shadows
119+
120+
Something doesn't look right in the second screenshot. The issue is that there are no shadows. In this section we will learn how shadows work and have a basic implemenation of them.
121+
122+
### Shadow Mapping
123+
124+
Most games use a technique known as "shadow mapping" to calculate shadows in their games. This technique dates back all the way to 1978. In shadow mapping, we first render the scene from the light's point of view into a depth map. This depth map is known as the shadow map. It looks something like this:
125+
126+
![The shadowmap](images/shadow_map.png)
127+
128+
The shadow map is in a coordinate system know as "shadow space". Then, we render the scene from the player's point of view. When doing the lighting calculations, we transform the fragment's to shadow space. We can use the XY coordinates of the transformed position to sample the depth from the shadowmap, and the comapre it to the current fragemnt's depth.
129+
130+
You can set the resolution of the shadow map. For example, if I wanted to set shadow map resolution to 1024, I would do this in composite:
131+
132+
```glsl
133+
const int shadowMapResolution = 1024;
134+
```
135+
136+
### The Shadow Pass
137+
138+
Optifine provides an optional shader stage called `shadow`. It runs for everything (blocks, entities, etc). Since we are only going to be recording the depth values to the depth map, it looks like this:
139+
140+
```glsl
141+
// shadow.vsh
142+
#version 120
143+
144+
void main(){
145+
gl_Position = ftransform();
146+
}
147+
148+
// shadow.fsh
149+
#version 120
150+
151+
void main() {}
152+
```
153+
154+
### Calculating the shadowing
155+
156+
In `composite`, we create a new function called `GetShadow`. It returns a floating point value that is 1 is the fragment is not in shadow, and 0 if it is.
157+
158+
```glsl
159+
float GetShadow(void){
160+
[...] // We will implement this
161+
}
162+
```
163+
164+
#### Reconstructing the position
165+
166+
Since we haven't written the fragment's position to a color texture, we can't just sample from one color texture and have the position that way. However, Optifine provides the depth texture taken from the eye's point of view. It is called `depthtex0` (and if you are wondering, there is `depthtex1` and `depthtex2`, we will look at those in the next chapter). We also have the texture coordinates, so we can contruct a clip space coordinate using:
167+
168+
```glsl
169+
vec3 ClipSpace = vec3(TexCoord, texture2D(depthtex0, TexCoord).r) * 2.0f - 1.0f;
170+
```
171+
172+
We have to move it to [-1, 1] from [0, 1] since `vec3(TexCoord, texture2D(depthtex0, TexCoord).r)` by itself is a screen space coordinate. Next, to get the view space coordinate, we have to muliply it by the inverse of the projection matrix. Optifine provides us a uniform for this called:
173+
174+
```glsl
175+
uniform mat4 gbufferProjectionInverse;
176+
```
177+
178+
We will also declare all the other matricies we will be needing:
179+
180+
```glsl
181+
uniform mat4 gbufferModelViewInverse;
182+
uniform mat4 shadowModelView;
183+
uniform mat4 shadowProjection;
184+
```
185+
186+
A quick note about matricies: The prefix `gbuffer*` is used for matricies used in the gbuffers programs, the prefix `shadow*` is used for the matricies in the shadow programs, the suffix `*Inverse` is used for inverse of any matrix, and the prefix `*Previous*` is used for the last frame's matrix. And Optifine `*ModelView*` matricies are a lie, they only contain the view matrix rotation and feet offset. `gl_ModelViewMatrix` in `shadow` and `gbuffers_*` contains the actual view matrix multiplied by the actual model matrix.
187+
188+
To convert from clip space to view scpace, we need to do this:
189+
190+
```glsl
191+
vec4 ViewW = gbufferProjectionInverse * vec4(ClipSpace, 1.0f);
192+
vec3 View = ViewW.xyz / ViewW.w;
193+
```
194+
195+
We divide by `w` to account for the inverse of the perspective divide. After that, we have to convert from view space to world space. We can do that by doing:
196+
197+
```glsl
198+
vec4 World = gbufferModelViewInverse * vec4(View, 1.0f);
199+
```
200+
201+
A note to the reader: this isn't actually world space, it's player space. It is centered around the player's feet.
202+
203+
After this we can convert directly to shadow space:
204+
205+
```glsl
206+
vec4 ShadowSpace = shadowProjection * shadowModelView * World;
207+
```
208+
209+
#### Checking for Shadowing
210+
211+
However this is not shadow "screen" space. However, we can covert it to shadow screen space easily:
212+
213+
```glsl
214+
vec3 SampleCoords = ShadowSpace.xyz * 0.5f + 0.5f;
215+
```
216+
217+
Then we can sample from the shadow map and do the comparison. The shadow map in this case is `shadowtex0`. `shadowtex1` exists, but we will look into that later as well. Also remember to declare `shadowtex0`:
218+
219+
```glsl
220+
uniform sampler2D shadowtex0;
221+
222+
float GetShadow(void){
223+
[...]
224+
return step(SampleCoords.z, texture2D(shadowtex0, SampleCoords.xy).r);
225+
}
226+
```
227+
228+
Then in `main`, we can do this:
229+
230+
```glsl
231+
vec3 Diffuse = Albedo * (LightmapColor + NdotL * GetShadow() + Ambient);
232+
```
233+
234+
If you reload the shader, you will probably get something that looks like this:
235+
236+
![Shadow acne](images/shadow_acne.png)
237+
238+
This weird thing is called shadow acne and is caused by a result of lack of shadow map information. We can fix this with a small bias in `GetShadow`:
239+
240+
```glsl
241+
float GetShadow(void){
242+
[...]
243+
return step(SampleCoords.z - 0.001f, texture2D(shadowtex0, SampleCoords.xy).r);
244+
}
245+
```
246+
247+
![Shadow bias](images/no_shadow_acne.png)
248+
249+
You may also notice that the shadows look blobby. This is because the resolution of our shadow map is low. In the next tutorial, we will look at fixing this issue using a technique called shadow distortion.
250+
251+
## Fixing the Sky
252+
253+
With the depth texture we can fix the sky very easily. The sky's depth is always 1.0, so right after we get the albedo, we can take advantage of early return to do this:
254+
255+
```glsl
256+
// Account for gamma correction
257+
vec3 Albedo = pow(texture2D(colortex0, TexCoords).rgb, vec3(2.2f));
258+
float Depth = texture2D(depthtex0, TexCoords).r;
259+
if(Depth == 1.0f){
260+
gl_FragData[0] = vec4(Albedo, 1.0f);
261+
return;
262+
}
263+
```
264+
265+
You can also pass in the sampled depth value to `GetShadow` for a small speed up:
266+
267+
```glsl
268+
float GetShadow(float depth) {
269+
vec3 ClipSpace = vec3(TexCoords, depth) * 2.0f - 1.0f;
270+
[...]
271+
}
272+
273+
vec3 Diffuse = Albedo * (LightmapColor + NdotL * GetShadow(Depth) + Ambient);
274+
```
275+
276+
![Fixed sky](images/fixed_sky.png)
277+
278+
## Conclusion
279+
280+
We have done shadow mapping and lightmap lighting in Minecraft shaders. In the next tutorial we will see how we can improve our shadows.
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#version 120
2+
3+
varying vec2 TexCoords;
4+
5+
// Direction of the sun (not normalized!)
6+
uniform vec3 sunPosition;
7+
8+
// The color textures which we wrote to
9+
uniform sampler2D colortex0;
10+
uniform sampler2D colortex1;
11+
uniform sampler2D colortex2;
12+
uniform sampler2D depthtex0;
13+
uniform sampler2D shadowtex0;
14+
15+
uniform mat4 gbufferProjectionInverse;
16+
uniform mat4 gbufferModelViewInverse;
17+
uniform mat4 shadowModelView;
18+
uniform mat4 shadowProjection;
19+
20+
/*
21+
const int colortex0Format = RGBA16F;
22+
const int colortex1Format = RGB16;
23+
const int colortex2Format = RGB16;
24+
*/
25+
26+
const float sunPathRotation = -40.0f;
27+
const int shadowMapResolution = 1024;
28+
29+
const float Ambient = 0.025f;
30+
31+
float AdjustLightmapTorch(in float torch) {
32+
const float K = 2.0f;
33+
const float P = 5.06f;
34+
return K * pow(torch, P);
35+
}
36+
37+
float AdjustLightmapSky(in float sky){
38+
float sky_2 = sky * sky;
39+
return sky_2 * sky_2;
40+
}
41+
42+
vec2 AdjustLightmap(in vec2 Lightmap){
43+
vec2 NewLightMap;
44+
NewLightMap.x = AdjustLightmapTorch(Lightmap.x);
45+
NewLightMap.y = AdjustLightmapSky(Lightmap.y);
46+
return NewLightMap;
47+
}
48+
49+
// Input is not adjusted lightmap coordinates
50+
vec3 GetLightmapColor(in vec2 Lightmap){
51+
// First adjust the lightmap
52+
Lightmap = AdjustLightmap(Lightmap);
53+
// Color of the torch and sky. The sky color changes depending on time of day but I will ignore that for simplicity
54+
const vec3 TorchColor = vec3(1.0f, 0.25f, 0.08f);
55+
const vec3 SkyColor = vec3(0.05f, 0.15f, 0.3f);
56+
// Multiply each part of the light map with it's color
57+
vec3 TorchLighting = Lightmap.x * TorchColor;
58+
vec3 SkyLighting = Lightmap.y * SkyColor;
59+
// Add the lighting togther to get the total contribution of the lightmap the final color.
60+
vec3 LightmapLighting = TorchLighting + SkyLighting;
61+
// Return the value
62+
return LightmapLighting;
63+
}
64+
65+
float GetShadow(float depth) {
66+
vec3 ClipSpace = vec3(TexCoords, depth) * 2.0f - 1.0f;
67+
vec4 ViewW = gbufferProjectionInverse * vec4(ClipSpace, 1.0f);
68+
vec3 View = ViewW.xyz / ViewW.w;
69+
vec4 World = gbufferModelViewInverse * vec4(View, 1.0f);
70+
vec4 ShadowSpace = shadowProjection * shadowModelView * World;
71+
vec3 SampleCoords = ShadowSpace.xyz * 0.5f + 0.5f;
72+
return step(SampleCoords.z - 0.001f, texture2D(shadowtex0, SampleCoords.xy).r);
73+
}
74+
75+
void main(){
76+
// Account for gamma correction
77+
vec3 Albedo = pow(texture2D(colortex0, TexCoords).rgb, vec3(2.2f));
78+
float Depth = texture2D(depthtex0, TexCoords).r;
79+
if(Depth == 1.0f){
80+
gl_FragData[0] = vec4(Albedo, 1.0f);
81+
return;
82+
}
83+
// Get the normal
84+
vec3 Normal = normalize(texture2D(colortex1, TexCoords).rgb * 2.0f - 1.0f);
85+
// Get the lightmap
86+
vec2 Lightmap = texture2D(colortex2, TexCoords).rg;
87+
vec3 LightmapColor = GetLightmapColor(Lightmap);
88+
// Compute cos theta between the normal and sun directions
89+
float NdotL = max(dot(Normal, normalize(sunPosition)), 0.0f);
90+
// Do the lighting calculations
91+
vec3 Diffuse = Albedo * (LightmapColor + NdotL * GetShadow(Depth) + Ambient);
92+
/* DRAWBUFFERS:0 */
93+
// Finally write the diffuse color
94+
gl_FragData[0] = vec4(Diffuse, 1.0f);
95+
}

0 commit comments

Comments
 (0)