diff options
| -rw-r--r-- | content/blog/blacklight_shader/index.md | 171 | ||||
| -rw-r--r-- | content/projects/nix_aurea/index.md | 11 | ||||
| -rw-r--r-- | public/blog/blacklight_shader/blacklight.png (renamed from content/blog/blacklight_shader/blacklight.png) | bin | 846587 -> 846587 bytes | |||
| -rw-r--r-- | public/blog/blacklight_shader/index.html | 227 | ||||
| -rw-r--r-- | public/index.html | 5 | ||||
| -rw-r--r-- | public/projects/games/NIX_AVREA/index.html | 26 |
6 files changed, 255 insertions, 185 deletions
diff --git a/content/blog/blacklight_shader/index.md b/content/blog/blacklight_shader/index.md deleted file mode 100644 index c5e4b27..0000000 --- a/content/blog/blacklight_shader/index.md +++ /dev/null @@ -1,171 +0,0 @@ -+++ -title = "creating a blacklight shader" -date = 2024-11-29 -[taxonomies] -projects = ["bevy_blacklight_material"] -+++ - -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™) - -i wanted to create a "blacklight" effect, where specific lights could reveal part of the base texture. this shader works with **spot lights** only, but could be extended to work with point lights - -; - -i wrote this shader in wgsl for a [bevy engine](https://bevyengine.org) project, but it should translate easily to other shading languages - -the finished shader can be found as part of [this repo](https://github.com/soaosdev/bevy_blacklight_material) -## shader inputs - -for this shader, i wanted the following features: -- the number of lights should be dynamic -- the revealed portion of the object should match the area illuminated by each light - - the falloff of the light over distance should match the fading of the object - -for this to work i need the following information about each light: -- position (world space) -- direction (world space) -- range -- inner and outer angle - - these will control the falloff of the light at its edges - - outer angle should be less than pi/2 radians - - inner angle should be less than the outer angle - -i also need some info from the vertex shader: -- position (**world space!**) -- uv - -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 - -lastly i'll take a base color texture and a sampler - -with all of that, i can start off the shader by setting up the inputs and fragment entry point: - -```wgsl -#import bevy_pbr::forward_io::VertexOutput; - -struct BlackLight { - position: vec3<f32>, - direction: vec3<f32>, - range: f32, - inner_angle: f32, - outer_angle: f32, -} - -@group(2) @binding(0) var<storage> lights: array<BlackLight>; -@group(2) @binding(1) var base_texture: texture_2d<f32>; -@group(2) @binding(2) var base_sampler: sampler; - -@fragment -fn fragment( - in: VertexOutput, -) -> @location(0) vec4<f32> { -} -``` -(bevy uses group 2 for custom shader bindings) - -since the number of lights is dynamic, i use a [storage buffer](https://google.github.io/tour-of-wgsl/types/arrays/runtime-sized-arrays/) to store that information - -## shader calculations - -the first thing we'll need to know is how close to looking at the fragment the light source is - -we can get this information using some interesting math: - -```wgsl -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)); -``` - -the first step of this is taking the dot product of light direction and the direction from the light to the fragment - -since both direction vectors are normalized, the dot product will be between -1.0 and 1.0 - -the dot product of two unit vectors is the cosine of the angle between them ([proof here](https://math.libretexts.org/Bookshelves/Calculus/Calculus_(OpenStax)/12%3A_Vectors_in_Space/12.03%3A_The_Dot_Product#Evaluating_a_Dot_Product)) - -therefore, we take the arccosine of that dot product to get the angle between the light and the fragment - -once we have this angle we can plug it in to a falloff based on the angle properties of the light: - -```wgsl -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))); -``` -```wgsl -fn linear_falloff_radius(factor: f32, radius: f32) -> f32 { - if factor < radius { - return 1.0; - } else { - return 1.0 - (factor - radius) / (1.0 - radius); - } -} -``` -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: - -```wgsl -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); -``` -```wgsl -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 < radius { - return 1.0; - } else { - return inverse_falloff((factor - radius) / (1.0 - radius)); - } -} -``` - -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: - -```wgsl -let base_color = textureSample(base_texture, base_sampler, in.uv); -let final_color = base_color * angle_factor * distance_factor; -``` - -this works for one light, but we need to refactor it to loop over all the provided blacklights: -```wgsl -@fragment -fn fragment( - in: VertexOutput, -) -> @location(0) vec4<f32> { - 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 < 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; -} -``` - -and with that, the shader is pretty much complete - -you can view the full completed shader code [here](https://github.com/soaosdev/bevy_blacklight_material/blob/master/assets/shaders/blacklight_material.wgsl) - -have fun! - diff --git a/content/projects/nix_aurea/index.md b/content/projects/nix_aurea/index.md deleted file mode 100644 index 2c68a6d..0000000 --- a/content/projects/nix_aurea/index.md +++ /dev/null @@ -1,11 +0,0 @@ -+++ -title = "nix_aurea" -[taxonomies] -categories = ["game"] -languages = ["rust"] -[extra] -wip = true -very_cool = true -+++ - -this page is under construction ;) diff --git a/content/blog/blacklight_shader/blacklight.png b/public/blog/blacklight_shader/blacklight.png Binary files differindex 2c5caf1..2c5caf1 100644 --- a/content/blog/blacklight_shader/blacklight.png +++ b/public/blog/blacklight_shader/blacklight.png diff --git a/public/blog/blacklight_shader/index.html b/public/blog/blacklight_shader/index.html new file mode 100644 index 0000000..db4b54e --- /dev/null +++ b/public/blog/blacklight_shader/index.html @@ -0,0 +1,227 @@ +<!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<f32>, + direction: vec3<f32>, + range: f32, + inner_angle: f32, + outer_angle: f32, + } + + @group(2) @binding(0) var<storage> lights: array<BlackLight>; + @group(2) @binding(1) var base_texture: texture_2d<f32>; + @group(2) @binding(2) var base_sampler: sampler; + + @fragment + fn fragment( + in: VertexOutput, + ) -> @location(0) vec4<f32> { + } + </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 < 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 < 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<f32> { + 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 < 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 diff --git a/public/index.html b/public/index.html index c4224ab..725c752 100644 --- a/public/index.html +++ b/public/index.html @@ -29,14 +29,13 @@ <h2>Stuff on this Server (Under Construction)</h2> <ul> <li> - <s>Projects</s> + <a href="/projects">Projects</a> </li> <li> <a href="https://git.soaos.dev" target="_blank">Gitea</a> </li> <li> - <s>Blog</s> (I wanna start posting more to my mastodon, might bring this back anyway, <a - href="https://indieweb.org/POSSE" target="_blank">POSSE</a> right?) + <a href="/blog">Blog</a> </li> <li> <a href="/things_i_like">Things I Like</a> diff --git a/public/projects/games/NIX_AVREA/index.html b/public/projects/games/NIX_AVREA/index.html new file mode 100644 index 0000000..47c914b --- /dev/null +++ b/public/projects/games/NIX_AVREA/index.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>NIX AVREA</title> +</head> + +<body> + <a href="/">Go Home</a> + <a href="..">Go Back</a> + <h1>NIX AVREA</h1> + <p>This is a project I've been working on since April 2024. It's probably the longest-running personal project I've ever done and has been a monumental undertaking so far.</p> + <p>I'm unsure how much I want to reveal about this project while I'm developing it, I want the experience to be as novel as possible once it's out. I think I'll probably stick to posting about it here on my site and the occasional YouTube video until it's closer to ready.</p> + <h2>About the Project</h2> + <p>NIX AVREA is the codename for my first game project. The game is highly experimental and features mechanics that (as far as I know) have never been attempted. The game is centered around dynamic content, using steganographic techniques to embed binary payloads inside of asset files in order to construct the game world from a directory on the player's filesystem.</p> + <p>There is a <i>ton</i> of stuff that's already implemented for this project and I'll gradually add more to the following directories explaining in-depth some of the components:</p> + <pre> ++-- <a href="#">mechanics/</a> ++-- <a href="#">design/</a> ++-- <a href="#">narrative/</a> + </pre> +</body> + +</html>
\ No newline at end of file |
