The scene starts simply for the development of the shader. It’s a MeshInstance
with a SphereMesh
resource, a Camera
with Current set to true, and a DirectionalLight
in a Spatial
scene.
To set up the deferred rendering chain, we need a viewport to hold a copy of the objects we want to be toon-lit.
ViewportContainer
named “ToonLightDataView” at the top of the scene tree.
0
.true
.1
.0
.Viewport
as a child of that ViewportContainer.
World
in the World property. Without this world, the objects in the light data will exist and appear inside the main scene.Duplicate your Sphere, Camera, and Light. Add these copies as children of the Viewport then add a RemoteTransform
to the sphere, camera, and light of the main scene to control those proxies.
The last two steps are to apply an appropriate material on the sphere copy to receive light and to emit color on the right channel.
SpatialMaterial
to the MeshInstance’s Material 0.
If we temporarily set the ViewportContainer’s self-modulate to white, we get this result:
On the sphere in the main world, add a new ShaderMaterial
, assign it a new Shader
, and let’s get coding. The shader is of type spatial with the unshaded render mode; all light data will come from the viewport.
We need two uniforms: one is the light data itself from the Viewport, and the other is a gradient ramp that gives control over the crispness of the shadow. We also add two uniforms that control the color of the shadow and the light independently.
shader_type spatial; render_mode unshaded; uniform sampler2D key_light_ramp : hint_black; uniform sampler2D light_data : hint_black; uniform vec4 base_color : hint_color; uniform vec4 key_light_color : hint_color; uniform vec4 shadow_color : hint_color;
Their defaults are black, so there’s no light data when they are missing. To access the light data, we sample the viewport texture’s fragment at the same spot as it exists on the main viewport. We do that with the SCREEN_UV
builtin. That’s why the data and the main viewport should both be the same size.
This data acts as a factor with which we sample from the light ramp. A light ramp is a one pixel high, long horizontal texture. By putting in the amount of light received from the viewport as the U axis of the UV, we decide at which point the light is dark enough that it should be all black, and at which point its light enough to be all white. The area between softens the ramp.
The closer together the white and black is, the sharper the transition.
void fragment() { vec3 diffuse = texture(light_data, SCREEN_UV).rgb; float key_light_value = texture(key_light_ramp, vec2(diffuse.r, 0)).r; }
We apply color in two steps: multiply the key light value by the key light color, then take the largest of that color and the shadow color. That way, it will never be any darker than the shadow color we specify.
vec3 out_color = base_color.rgb * key_light_value * key_light_color.rgb; out_color = max(out_color, shadow_color.rgb); ALBEDO = out_color;
Note on SCREEN_UV: The shader runs while in the editor, and the SCREEN_UV builtin uses the editor viewport size to calculate those values. But our light data viewport is the size of the game resolution in project settings and is from the viewpoint of the camera in the scene tree which results in a broken look in the editor.
I’ll show how you can build a Godot plugin to fix this problem in another tutorial. For now, a workaround is to set the Width and Height in Project Settings to one that matches the Viewport size in the editor, and set the Test Width and Test Height to the actual game resolution. Then select your camera and use the Preview checkbox. Or you can run the game to see the shader in action.
Once the ViewportTexture
is pointing at the Viewport of the ToonLightDataView container (you may have to turn on the Local to Scene under the Resource tab in the material) and there is an appropriate GradientTexture
in the Key Light Ramp parameter, you have a nicely cel-shaded sphere. You can play with the colors and the softness of the key light shadow transition without affecting anything else.
shader_type spatial; render_mode unshaded; //Data textures uniform sampler2D light_data : hint_black; uniform sampler2D key_light_ramp : hint_black; //Light colors uniform vec4 key_light_color : hint_color; uniform vec4 shadow_color : hint_color; void fragment() { //Data vec3 diffuse = texture(light_data, SCREEN_UV).rgb; //Key light float key_light_value = texture(key_light_ramp, vec2(diffuse.r, 0)).r; vec3 out_color = key_light_value * key_light_color.rgb; out_color = max(out_color, shadow_color.rgb); ALBEDO = out_color; }
Pro-tip: Cast shadows by enabling shadows on the key light in the light data viewport and setting the viewport’s shadow atlas size to be greater than 0.