summaryrefslogtreecommitdiff
path: root/public/blog/blacklight_shader/index.html
diff options
context:
space:
mode:
Diffstat (limited to 'public/blog/blacklight_shader/index.html')
-rw-r--r--public/blog/blacklight_shader/index.html227
1 files changed, 0 insertions, 227 deletions
diff --git a/public/blog/blacklight_shader/index.html b/public/blog/blacklight_shader/index.html
deleted file mode 100644
index db4b54e..0000000
--- a/public/blog/blacklight_shader/index.html
+++ /dev/null
@@ -1,227 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Creating a Blacklight Shader - soaos</title>
-</head>
-
-<body>
- <a href="/">Go Home</a>
- <a href="..">Go Back</a>
- <h1>Creating a Blacklight Shader</h1>
- <font color="red">
- <b>NOTE: THIS POST WAS TRANSFERRED FROM MARKDOWN BY HAND SO I MIGHT HAVE MISSED SOME STUFF SORRY</b>
- </font>
- <p>today i wanted to take a bit of time to write about a shader i implemented for my in-progress game project (more
- on that soon™)</p>
- <p>i wanted to create a "blacklight" effect, where specific lights could reveal part of the base texture. this
- shader works with <b>spot lights</b> only, but could be extended to work with point lights</p>
- <figure>
- <img src="blacklight.png" alt="Example of shader running, showing hidden writing on a wall" width="100%">
- <figcaption>Example of shader running, showing hidden writing on a wall.</figcaption>
- </figure>
-
- <p>i wrote this shader in wgsl for a <a href="https://bevyengine.org" target="_blank">bevy engine</a> project, but
- it should translate easily to other shading languages</p>
-
- <p>the finished shader can be found as part of <a href="https://git.soaos.dev/soaos/bevy_blacklight_material"
- target="_blank">this repo</a></p>
-
- <h2>shader inputs</h2>
-
- <p>
- for this shader, i wanted the following features:
- <ul>
- <li>
- the number of lights should be dynamic
- </li>
- <li>
- the revealed portion of the object should match the area illuminated by each light
- </li>
- <li>
- the falloff of the light over distance should match the fading of the object
- </li>
- </ul>
-
- for this to work i need the following information about each light:
- <ul>
- <li>
- position (world space)
- </li>
- <li>
- direction (world space)
- </li>
- <li>
- range
- </li>
- <li>
- inner and outer angle
- </li>
- <li>
- these will control the falloff of the light at its edges
- </li>
- <li>
- outer angle should be less than pi/2 radians
- </li>
- <li>
- inner angle should be less than the outer angle
- </li>
- </ul>
-
- i also need some info from the vertex shader:
- <ul>
- <li>
- position (<b>world space!</b>)
- </li>
- <li>
- uv
- </li>
- </ul>
- </p>
- <p>bevy's default pbr vertex shader provides this information, but as long as you can get this info into your
- fragment
- shader you should be good to go</p>
-
- <p>lastly i'll take a base color texture and a sampler</p>
-
- <p>
- with all of that, i can start off the shader by setting up the inputs and fragment entry point:
-
- <pre>
- #import bevy_pbr::forward_io::VertexOutput;
-
- struct BlackLight {
- position: vec3&lt;f32&gt;,
- direction: vec3&lt;f32&gt;,
- range: f32,
- inner_angle: f32,
- outer_angle: f32,
- }
-
- @group(2) @binding(0) var&lt;storage&gt; lights: array&lt;BlackLight&gt;;
- @group(2) @binding(1) var base_texture: texture_2d&lt;f32&gt;;
- @group(2) @binding(2) var base_sampler: sampler;
-
- @fragment
- fn fragment(
- in: VertexOutput,
- ) -> @location(0) vec4&lt;f32&gt; {
- }
- </pre>
- (bevy uses group 2 for custom shader bindings)
- </p>
-
- <p>
- since the number of lights is dynamic, i use a <a
- href="https://google.github.io/tour-of-wgsl/types/arrays/runtime-sized-arrays/">storage buffer</a> to store
- that information
- </p>
-
- <h2>shader calculations</h2>
-
- <p>the first thing we'll need to know is how close to looking at the fragment the light source
- is</p>
-
- <p>
- we can get this information using some interesting math:
-
- <pre>
- let light = lights[0];
- let light_to_fragment_direction = normalize(in.world_position.xyz - light.position);
- let light_to_fragment_angle = acos(dot(light.direction, light_to_fragment_direction));
- </pre>
-
- the first step of this is taking the dot product of light direction and the direction from
- the light to the fragment
- </p>
-
- <p>since both direction vectors are normalized, the dot product will be between -1.0 and 1.0</p>
-
- <p>
- the dot product of two unit vectors is the cosine of the angle between them (<a
- href="https://math.libretexts.org/Bookshelves/Calculus/Calculus_(OpenStax)/12%3A_Vectors_in_Space/12.03%3A_The_Dot_Product#Evaluating_a_Dot_Product">proof
- here</a>)
- </p>
-
- <p>
- therefore, we take the arccosine of that dot product to get the angle between the light and
- the fragment
- </p>
-
- <p>
- once we have this angle we can plug it in to a falloff based on the angle properties of the
- light:
-
- <pre>
- let angle_inner_factor = light.inner_angle/light.outer_angle;
- let angle_factor = linear_falloff_radius(light_to_fragment_angle / light.outer_angle, angle_inner_factor);
- </pre>
- <pre>
- fn linear_falloff_radius(factor: f32, radius: f32) -> f32 {
- if factor &lt; radius { return 1.0; } else {
- return 1.0 - (factor - radius) / (1.0 - radius);
- }
- }
- </pre>
- </p>
- <p>
- next, we need to make sure the effect falls off properly over distance we can do this by getting the distance
- from the light to
- the fragment and normalizing it with the range of the light before plugging that into an inverse square falloff
- we'll use squared distance to avoid expensive and unnecessary square root operations:
- <pre>
- let light_distance_squared=distance_squared(in.world_position.xyz, light.position);
- let distance_factor=inverse_falloff_radius(saturate(light_distance_squared / (light.range * light.range)), 0.5);
- </pre>
- <pre>
- fn distance_squared(a: vec3f, b: vec3f) -> f32 {
- let vec = a - b;
- return dot(vec, vec);
- }
-
- fn inverse_falloff(factor: f32) -> f32 {
- return pow(1.0 - factor, 2.0);
- }
-
- fn inverse_falloff_radius(factor: f32, radius: f32) -> f32 {
- if factor &lt; radius { return 1.0; } else {
- return inverse_falloff((factor - radius) / (1.0 - radius));
- }
- }
- </pre>
- </p>
- <p>
- now we'll have a float multiplier between 0.0 and 1.0 for our angle and distance to the light we can get the
- resulting color by multiplying these with the base color texture:
- <pre>
- let base_color = textureSample(base_texture, base_sampler, in.uv);
- let final_color=base_color * angle_factor * distance_factor;
- </pre>
- this works for one light, but we need to refactor it to loop over all the provided blacklights:
- <pre>
-
- @fragment fn fragment( in: VertexOutput ) -> @location(0) vec4&lt;f32&gt; {
- let base_color = textureSample(base_texture, base_sampler, in.uv);
- var final_color = vec4f(0.0, 0.0, 0.0, 0.0);
- for (var i = u32(0); i &lt; arrayLength(&lights); i = i+1) {
- let light=lights[i];
- let light_to_fragment_direction = normalize(in.world_position.xyz - light.position);
- let light_to_fragment_angle = acos(dot(light.direction, light_to_fragment_direction));
- let angle_inner_factor = light.inner_angle / light.outer_angle;
- let angle_factor = linear_falloff_radius(light_to_fragment_angle / light.outer_angle, angle_inner_factor);
- let light_distance_squared = distance_squared(in.world_position.xyz, light.position);
- let distance_factor = inverse_falloff_radius(saturate(light_distance_squared / (light.range * light.range)), 0.5);
- final_color = saturate(final_color + base_color * angle_factor * distance_factor);
- }
- return final_color;
- }
- </pre>
- and with that, the shader is pretty much complete you can view the full completed shader code <a
- href="https://github.com/soaosdev/bevy_blacklight_material/blob/master/assets/shaders/blacklight_material.wgsl">here</a>
- </p>
- <p>have fun!</p>
-</body>
-
-</html> \ No newline at end of file