Created
February 24, 2020 16:05
-
-
Save optozorax/b62122a06e38f8752ee4e95879ecc1db to your computer and use it in GitHub Desktop.
miniquad_shader_1.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Shader: https://www.shadertoy.com/view/4ljGRd | |
Post: https://t.me/optozorax_dev/62 | |
Sorry for shitcode, this is just test | |
Based on `quad` example in miniquad | |
*/ | |
use miniquad::*; | |
#[repr(C)] | |
struct Vec2 { | |
x: f32, | |
y: f32, | |
} | |
#[repr(C)] | |
struct Vertex { | |
pos: Vec2, | |
uv: Vec2, | |
} | |
struct Stage { | |
pipeline: Pipeline, | |
bindings: Bindings, | |
time: f64, | |
} | |
impl Stage { | |
pub fn new(ctx: &mut Context) -> Stage { | |
#[rustfmt::skip] | |
let vertices: [Vertex; 4] = [ | |
Vertex { pos : Vec2 { x: -1.0, y: 1.0 }, uv: Vec2 { x: 0., y: 0. } }, | |
Vertex { pos : Vec2 { x: 1.0, y: 1.0 }, uv: Vec2 { x: 1., y: 0. } }, | |
Vertex { pos : Vec2 { x: 1.0, y: -1.0 }, uv: Vec2 { x: 1., y: 1. } }, | |
Vertex { pos : Vec2 { x: -1.0, y: -1.0 }, uv: Vec2 { x: 0., y: 1. } }, | |
]; | |
let vertex_buffer = Buffer::immutable(ctx, BufferType::VertexBuffer, &vertices); | |
let indices: [u16; 6] = [0, 1, 2, 0, 2, 3]; | |
let index_buffer = Buffer::immutable(ctx, BufferType::IndexBuffer, &indices); | |
let pixels: [u8; 4 * 4 * 4] = [ | |
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, | |
0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, | |
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, | |
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | |
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, | |
]; | |
let texture = Texture::from_rgba8(ctx, 4, 4, &pixels); | |
let bindings = Bindings { | |
vertex_buffers: vec![vertex_buffer], | |
index_buffer, | |
images: vec![texture], | |
}; | |
let shader = Shader::new(ctx, shader::VERTEX, shader::FRAGMENT, shader::META); | |
let pipeline = Pipeline::new( | |
ctx, | |
&[BufferLayout::default()], | |
&[ | |
VertexAttribute::new("pos", VertexFormat::Float2), | |
VertexAttribute::new("uv", VertexFormat::Float2), | |
], | |
shader, | |
); | |
Stage { pipeline, bindings, time: -1.0 } | |
} | |
} | |
impl EventHandler for Stage { | |
fn update(&mut self, _ctx: &mut Context) { } | |
#[allow(clippy::float_cmp)] | |
fn draw(&mut self, ctx: &mut Context) { | |
if self.time == -1.0 { | |
self.time = date::now(); | |
} | |
let t = (date::now() - self.time) * 10.0; | |
ctx.begin_default_pass(Default::default()); | |
ctx.apply_pipeline(&self.pipeline); | |
ctx.apply_bindings(&self.bindings); | |
let size = ctx.screen_size(); | |
let scale = 0.6; | |
ctx.apply_uniforms(&shader::Uniforms { | |
iResolution: (scale * size.0 / (size.1.max(size.0)), -scale * size.1 / (size.1.max(size.0)), 0.0), | |
iTime: t as f32, | |
}); | |
ctx.draw(0, 6, 1); | |
ctx.end_render_pass(); | |
ctx.commit_frame(); | |
} | |
} | |
fn main() { | |
miniquad::start(conf::Conf::default(), |mut ctx| { | |
UserData::owning(Stage::new(&mut ctx), ctx) | |
}); | |
} | |
mod shader { | |
use miniquad::*; | |
pub const VERTEX: &str = r#"#version 100 | |
attribute vec2 pos; | |
attribute vec2 uv; | |
varying lowp vec2 texcoord; | |
void main() { | |
gl_Position = vec4(pos, 0, 1); | |
texcoord = uv; | |
}"#; | |
pub const FRAGMENT: &str = r#"#version 100 | |
precision highp float; | |
varying lowp vec2 texcoord; | |
//uniform sampler2D tex; | |
uniform vec3 iResolution; | |
uniform float iTime; | |
//uniform vec4 iMouse; | |
// Ray tracing is a topic I have always wanted to explore, but never really had | |
// the opportunity to do so until now. What exactly is ray tracing? Consider a | |
// lamp hanging from the ceiling. Light is constantly being emitted from the | |
// lamp in the form of light rays, which bounce around the room until they hit | |
// your eye. Ray tracing follows a similar concept by simulating the path of | |
// light through a scene, except in reverse. There is no point in doing the math | |
// for light rays you cannot see! | |
// Algorithmically, ray tracing is very elegant. For each pixel, shoot a light | |
// ray from the camera through each pixel on screen. If the ray collides with | |
// geometry in the scene, create new rays that perform the same process for both | |
// reflection, as in a mirror, and refraction, as in through water. Repeat | |
// to your satisfaction. | |
// Having worked extensively with OpenCL in the past, this seemed like a good | |
// candidate to port to a parallel runtime on a GPU. Inspired by the [smallpt](http://www.kevinbeason.com/smallpt/#moreinfo) | |
// line-by-line explanation, I decided to write a parallel ray tracer with | |
// extensive annotations. The results are below ... | |
//  | |
// I start with a simple ray definition, consisting of an origin point and a | |
// direction vector. I also define a directional light to illuminate my scene. | |
struct Ray { | |
vec3 origin; | |
vec3 direction; | |
}; | |
struct Light { | |
vec3 color; | |
vec3 direction; | |
}; | |
// In real life, objects have many different material properties. Some objects | |
// respond very differently to light than others. For instance, a sheet of paper | |
// and a polished mirror. The former exhibits a strong *diffuse* response; | |
// incoming light is reflected at many angles. The latter is an example of a | |
// *specular* response, where incoming light is reflected in a single direction. | |
// To model this, I create a basic material definition. Objects in my scene | |
// share a single (RGB) color with diffuse and specular weights. | |
struct Material { | |
vec3 color; | |
float diffuse; | |
float specular; | |
}; | |
// To render the scene, I need to know where a ray intersects with an object. | |
// Since rays have infinite length from an origin, I can model the point of | |
// intersection by storing the distance along the ray. I also need to store the | |
// surface normal so I know which way to bounce! Once I create a ray, it loses | |
// the concept of scene geometry, so one more thing I do is forward the surface | |
// material properties. | |
struct Intersect { | |
float len; | |
vec3 normal; | |
Material material; | |
}; | |
// The last data structures I create are for objects used to fill my scene. The | |
// most basic object I can model is a sphere, which is defined as a radius at | |
// some center position, with some material properties. To draw the floor, I | |
// also define a simple horizontal plane centered at the origin, with a normal | |
// vector pointing upwards. | |
struct Sphere { | |
float radius; | |
vec3 position; | |
Material material; | |
}; | |
struct Plane { | |
vec3 normal; | |
Material material; | |
}; | |
// At this point, I define some global variables. A more advanced program might | |
// pass these values in as uniforms, but for now, this is easier to tinker with. | |
// Due to floating point precision errors, when a ray intersects geometry at a | |
// surface, the point of intersection could possibly be just below the surface. | |
// The subsequent reflection ray would then bounce off the *inside* wall of the | |
// surface. This is known as self-intersection. When creating new rays, I | |
// initialize them at a slightly offset origin to help mitigate this problem. | |
const float epsilon = 1e-3; | |
// The classical ray tracing algorithm is recursive. However, GLSL does not | |
// support recursion, so I instead use an iterative approach to control the | |
// number of light bounces. | |
const int iterations = 16; | |
// Next, I define an exposure time and gamma value. At this point, I also create | |
// a basic directional light and define the ambient light color; the color here | |
// is mostly a matter of taste. Basically ... lighting controls. | |
const float exposure = 1e-2; | |
const float gamma = 2.2; | |
const float intensity = 100.0; | |
const vec3 ambient = vec3(0.6, 0.8, 1.0) * intensity / gamma; | |
// For a Static Light | |
Light light = Light(vec3(1.0) * intensity, normalize(vec3(-1.0, 0.75, 1.0))); | |
// For a Rotating Light | |
// Light light = Light(vec3(1.0) * intensity, normalize( | |
// vec3(-1.0 + 4.0 * cos(iTime), 4.75, | |
// 1.0 + 4.0 * sin(iTime)))); | |
// I strongly dislike this line. I needed to know when a ray hits or misses a | |
// surface. If it hits geometry, I returned the point at the surface. Otherwise, | |
// the ray misses all geometry and instead hits the sky box. In a language that | |
// supports dynamic return values, I could `return false`, but that is not an | |
// option in GLSL. In the interests of making progress, I created an intersect | |
// of distance zero to represent a miss and moved on. | |
const Intersect miss = Intersect(0.0, vec3(0.0), Material(vec3(0.0), 0.0, 0.0)); | |
// As indicated earlier, I implement ray tracing for spheres. I need to compute | |
// the point at which a ray intersects with a sphere. [Line-Sphere](http://en.wikipedia.org/wiki/Line-sphere_intersection) | |
// intersection is relatively straightforward. For reflection purposes, a ray | |
// either hits or misses, so I need to check for no solutions, or two solutions. | |
// In the latter case, I need to determine which solution is "in front" so I can | |
// return an intersection of appropriate distance from the ray origin. | |
Intersect intersect(Ray ray, Sphere sphere) { | |
// Check for a Negative Square Root | |
vec3 oc = sphere.position - ray.origin; | |
float l = dot(ray.direction, oc); | |
float det = pow(l, 2.0) - dot(oc, oc) + pow(sphere.radius, 2.0); | |
if (det < 0.0) return miss; | |
// Find the Closer of Two Solutions | |
float len = l - sqrt(det); | |
if (len < 0.0) len = l + sqrt(det); | |
if (len < 0.0) return miss; | |
return Intersect(len, (ray.origin + len*ray.direction - sphere.position) / sphere.radius, sphere.material); | |
} | |
// Since I created a floor plane, I likewise have to handle reflections for | |
// planes by implementing [Line-Plane](http://en.wikipedia.org/wiki/Line-plane_intersection) | |
// intersection. I only care about the intersect for the purposes of reflection, | |
// so I only check if the quotient is non-zero. | |
Intersect intersect(Ray ray, Plane plane) { | |
float len = -dot(ray.origin, plane.normal) / dot(ray.direction, plane.normal); | |
if (len < 0.0) return miss; | |
return Intersect(len, plane.normal, plane.material); | |
} | |
// In a *real* ray tracing renderer, geometry would be passed in from the host | |
// as a mesh containing vertices, normals, and texture coordinates, but for the | |
// sake of simplicity, I hand-coded the scene-graph. In this function, I take an | |
// input ray and iterate through all geometry to determine intersections. | |
Intersect trace(Ray ray) { | |
const int num_spheres = 3; | |
Sphere spheres[num_spheres]; | |
// I initially started with the [smallpt](www.kevinbeason.com/smallpt/) | |
// scene definition, but soon found performance was abysmal on very large | |
// spheres. I kept the general format, modified to fit my data structures. | |
spheres[0] = Sphere(2.0, vec3(-4.0, 3.0 + sin(iTime), 0), Material(vec3(1.0, 0.0, 0.2), 1.0, 0.001)); | |
spheres[1] = Sphere(3.0, vec3( 4.0 + cos(iTime), 3.0, 0), Material(vec3(0.0, 0.2, 1.0), 1.0, 0.0)); | |
spheres[2] = Sphere(1.0, vec3( 0.5, 1.0, 6.0), Material(vec3(1.0, 1.0, 1.0), 0.5, 0.25)); | |
// Since my ray tracing approach involves drawing to a 2D quad, I can no | |
// longer use the OpenGL Depth and Stencil buffers to control the draw | |
// order. Drawing is therefore sensitive to z-indexing, so I first intersect | |
// with the plane, then loop through all spheres back-to-front. | |
Intersect intersection = miss; | |
Intersect plane = intersect(ray, Plane(vec3(0, 1, 0), Material(vec3(1.0, 1.0, 1.0), 1.0, 0.0))); | |
if (plane.material.diffuse > 0.0 || plane.material.specular > 0.0) { intersection = plane; } | |
for (int i = 0; i < num_spheres; i++) { | |
Intersect sphere = intersect(ray, spheres[i]); | |
if (sphere.material.diffuse > 0.0 || sphere.material.specular > 0.0) | |
intersection = sphere; | |
} | |
return intersection; | |
} | |
// This is the critical part of writing a ray tracer. I start with some empty | |
// scratch vectors for color data and the Fresnel factor. I trace the scene with | |
// using an input ray, and continue to fire new rays until the iteration depth | |
// is reached, at which point I return the total sum of the color values from | |
// computed at each bounce. | |
vec3 radiance(Ray ray) { | |
vec3 color = vec3(0.0), fresnel = vec3(0.0); | |
vec3 mask = vec3(1.0); | |
for (int i = 0; i <= iterations; ++i) { | |
Intersect hit = trace(ray); | |
// This goes back to the dummy "miss" intersect. Basically, if the scene | |
// trace returns an intersection with either a diffuse or specular | |
// coefficient, then it has encountered a surface of a sphere or plane. | |
// Otherwise, the current ray has reached the ambient-colored sky box. | |
if (hit.material.diffuse > 0.0 || hit.material.specular > 0.0) { | |
// Here I use the [Schlick Approximation](http://en.wikipedia.org/wiki/Schlick's_approximation) | |
// to determine the Fresnel specular contribution factor, a measure | |
// of how much incoming light is reflected or refracted. I compute | |
// the Fresnel term and use a mask to track the fraction of | |
// reflected light in the current ray with respect to the original. | |
vec3 r0 = hit.material.color.rgb * hit.material.specular; | |
float hv = clamp(dot(hit.normal, -ray.direction), 0.0, 1.0); | |
fresnel = r0 + (1.0 - r0) * pow(1.0 - hv, 5.0); | |
mask *= fresnel; | |
// I handle shadows and diffuse colors next. I condensed this part | |
// into one conditional evaluation for brevity. Remember `epsilon`? | |
// I use it to trace a ray slightly offset from the point of | |
// intersection to the light source. If the shadow ray does not hit | |
// an object, it will be a "miss" as it hits the skybox. This means | |
// there are no objects between the point and the light, at which | |
// point I can add the diffuse color to the fragment color since the | |
// object is not in shadow. | |
if (trace(Ray(ray.origin + hit.len * ray.direction + epsilon * light.direction, light.direction)) == miss) { | |
color += clamp(dot(hit.normal, light.direction), 0.0, 1.0) * light.color | |
* hit.material.color.rgb * hit.material.diffuse | |
* (1.0 - fresnel) * mask / fresnel; | |
} | |
// After computing diffuse colors, I then generate a new reflection | |
// ray and overwrite the original ray that was passed in as an | |
// argument to the radiance(...) function. Then I repeat until I | |
// reach the iteration depth. | |
vec3 reflection = reflect(ray.direction, hit.normal); | |
ray = Ray(ray.origin + hit.len * ray.direction + epsilon * reflection, reflection); | |
} else { | |
// This is the other half of the tracing branch. If the trace failed | |
// to return an intersection with an attached material, then it is | |
// safe to assume that the ray points at the sky, or out of bounds | |
// of the scene. At this point I realized that real objects have a | |
// small sheen to them, so I hard-coded a small spotlight pointing | |
// in the same direction as the main light for pseudo-realism. | |
vec3 spotlight = vec3(1e6) * pow(abs(dot(ray.direction, light.direction)), 250.0); | |
color += mask * (ambient + spotlight); break; | |
} | |
} | |
return color; | |
} | |
// The main function primarily deals with organizing data from OpenGL into a | |
// format that the ray tracer can use. For ray tracing, I need to fire a ray for | |
// each pixel, or more precisely, a ray for every fragment. However, pixels to | |
// fragment coordinates do not map one a one-to-one basis, so I need to divide | |
// the fragment coordinates by the viewport resolution. I then offset that by a | |
// fixed value to re-center the coordinate system. | |
void main() { | |
vec2 fragCoord = texcoord; | |
vec2 uv = fragCoord.xy / iResolution.xy + vec2(-0.8, 1); | |
uv.x *= -iResolution.x / iResolution.y; | |
uv.y *= -iResolution.y / iResolution.x; | |
// For each fragment, create a ray at a fixed point of origin directed at | |
// the coordinates of each fragment. The last thing before writing the color | |
// to the fragment is to post-process the pixel values using tone-mapping. | |
// In this case, I adjust for exposure and perform linear gamma correction. | |
Ray ray = Ray(vec3(0.0, 2.5, 12.0), normalize(vec3(uv.x, uv.y, -1.0))); | |
gl_FragColor = vec4(pow(radiance(ray) * exposure, vec3(1.0 / gamma)), 1.0); | |
} | |
"#; | |
pub const META: ShaderMeta = ShaderMeta { | |
images: &[], | |
uniforms: UniformBlockLayout { | |
uniforms: &[ | |
("iResolution", UniformType::Float3), | |
("iTime", UniformType::Float1), | |
], | |
}, | |
}; | |
#[allow(non_snake_case)] | |
#[repr(C)] | |
pub struct Uniforms { | |
pub iResolution: (f32, f32, f32), | |
pub iTime: f32, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment