├── .gitignore ├── README.md ├── chapters ├── 01-hello-world.glsl ├── 01-hello-world.md ├── 03-rgb.glsl ├── 03-rgb.md ├── 04-swizzling.glsl ├── 04-swizzling.md ├── 04b-maths-sorry.glsl ├── 04b-maths-sorry.md ├── 05-square.glsl ├── 05-square.md ├── 06-circle.glsl ├── 06-circle.md ├── 07-distance-fields.glsl ├── 07-distance-fields.md ├── 08-moving-shapes.glsl ├── 08-moving-shapes.md ├── 09-combining-shapes.glsl ├── 09-combining-shapes.md ├── 09b-blending-shapes.glsl ├── 09b-blending-shapes.md ├── 10-repeating-space.glsl ├── 10-repeating-space.md ├── 10b-primitives.glsl ├── 10b-primitives.md ├── 11-add-a-dimension.glsl ├── 11-add-a-dimension.md ├── 12-combining-again.glsl ├── 12-combining-again.md ├── 13-blending-again.glsl ├── 13-blending-again.md ├── 14-repeating-space.glsl ├── 14-repeating-space.md ├── 15-repeating-space.glsl ├── 15-repeating-space.md └── _template.html ├── display.js ├── editor-mode-glsl.js ├── editor.js ├── index.css ├── index.html ├── index.js ├── lib ├── build.js ├── generate.js ├── process-shader.js └── serve.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /bundle.js 2 | /chapters/*.html 3 | !/chapters/_template.html 4 | node_modules 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fragment-foundry 2 | 3 | An introduction to fragment shaders and signed distance functions, available [right in your browser](https://hughsk.io/fragment-foundry/). 4 | 5 |  6 | 7 | First written for [Electrofringe 2016](http://electrofringe.net/) in Sydney, Australia. 8 | 9 | If this piques your interest, it's also worth checking out the following projects: 10 | 11 | * [shader-school](https://github.com/stackgl/shader-school) for vertex *and* fragment shaders and their normal usage. 12 | * [webgl-workshop](https://github.com/stackgl/webgl-workshop) for more on the WebGL API. 13 | * [The Book of Shaders](http://thebookofshaders.com/) for fragment shaders in more depth. 14 | -------------------------------------------------------------------------------- /chapters/01-hello-world.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | // 3 | // Change this shader (displayed in the top-left box) to 4 | // make it green instead of red. The correct answer's result 5 | // is displayed in the bottom-left box. 6 | // 7 | // Note you might want to read through the information on the 8 | // right to get familiar with the basics first! 9 | // 10 | vec4 green = vec4(0, 1, 0, 1); 11 | vec4 blue = vec4(0, 0, 1, 1); 12 | vec4 red = vec4(1, 0, 0, 1); 13 | 14 | void main() { 15 | gl_FragColor = red; 16 | } 17 | #pragma solution 18 | vec4 green = vec4(0, 1, 0, 1); 19 | vec4 blue = vec4(0, 0, 1, 1); 20 | vec4 red = vec4(1, 0, 0, 1); 21 | 22 | void main() { 23 | gl_FragColor = green; 24 | } 25 | -------------------------------------------------------------------------------- /chapters/01-hello-world.md: -------------------------------------------------------------------------------- 1 | # Hello World! 2 | 3 | Hey, welcome! This is a self-guided workshop introducing you to the magic of *fragment shaders*. 4 | 5 | ## How This Works 6 | 7 | Each exercise in the workshop has a few parts. Here you'll find some background information that'll help you work through the task. 8 | 9 | In the middle, you'll see a shader editor: your goal is to fix the shader as described in the code. You can see a preview on the left, both for your current answer and for the correct one. Once they match up, you'll pass the lesson. 10 | 11 | Good luck! 12 | 13 | ## Wait, Shaders? 14 | 15 | Most of the software we write and run — JavaScript, C#, Java, etc. — is executed on the computer's Central Processing Unit (CPU). This is your computer's brain, responsible for generic computation. 16 | 17 | However there's another processing unit on your machine available to you: the Graphics Processing Unit (GPU). It's designed from the ground up to be *really* good at crunching numbers for computer graphics, and can do so much faster than your CPU. Its performance boost comes from the hardware. While a CPU has 2–8 big cores, a GPU has hundreds or even thousands of small ones. This makes it great at running code in *parallel*: provided a thread doesn't need to know anything about its neighbours, you can run a whole bunch of them really quickly at the same time without waiting for the others to finish. 18 | 19 | This concept is perhaps better explained by Mythbusters' Adam Savage and Jamie Hyneman: 20 | 21 | 22 | 23 | Shaders are the tiny programs that run on your GPU. There's a bunch of different types of shaders and shader languages with different purposes, but today we're looking at GLSL fragment shaders. 24 | 25 | GLSL is the shader language used for WebGL, meaning we can run it here in the browser for you. 26 | 27 | Fragment shaders are responsible for giving each pixel their colour. While they were originally intended simply for applying lighting effects to objects, they've since been pushed to their limits in communities such as [Shadertoy](https://www.shadertoy.com/). Take a look: all of the demos there are drawn completely in code. No assets to be seen! This is a popular technique in the demoscene for creating elaborate scenes with a small amount of code. 28 | 29 | This one fits in 4 kilobytes: 30 | 31 | 32 | -------------------------------------------------------------------------------- /chapters/03-rgb.glsl: -------------------------------------------------------------------------------- 1 | #pragma prefix 2 | uniform vec2 iResolution; 3 | #pragma question 4 | // 5 | // Fix the color variables listed here so that their 6 | // values match their name. 7 | // 8 | // You should only need a combination of 0s and 1s :) 9 | // 10 | 11 | vec3 red = vec3(1, 1, 1); 12 | vec3 green = vec3(1, 1, 1); 13 | vec3 blue = vec3(1, 1, 1); 14 | vec3 cyan = vec3(1, 1, 1); 15 | vec3 magenta = vec3(1, 1, 1); 16 | vec3 yellow = vec3(1, 1, 1); 17 | vec3 white = vec3(1, 1, 1); 18 | #pragma solution 19 | vec3 red = vec3(1, 0, 0); 20 | vec3 green = vec3(0, 1, 0); 21 | vec3 blue = vec3(0, 0, 1); 22 | vec3 cyan = vec3(0, 1, 1); 23 | vec3 magenta = vec3(1, 0, 1); 24 | vec3 yellow = vec3(1, 1, 0); 25 | vec3 white = vec3(1, 1, 1); 26 | #pragma suffix 27 | float aastep(float threshold, float value) { 28 | #ifdef GL_OES_standard_derivatives 29 | float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757; 30 | return smoothstep(threshold-afwidth, threshold+afwidth, value); 31 | #else 32 | return step(threshold, value); 33 | #endif 34 | } 35 | 36 | #define PI 3.14159265359 37 | vec2 rt (float r, float a) { 38 | a *= PI * 2.0; 39 | return r * vec2(sin(a), cos(a)); 40 | } 41 | 42 | vec2 squareFrame(vec2 screenSize, vec2 coord) { 43 | vec2 position = 2.0 * (coord.xy / screenSize.xy) - 1.0; 44 | if (screenSize.x > screenSize.y) { 45 | position.x *= screenSize.x / screenSize.y; 46 | } else { 47 | position.y *= screenSize.y / screenSize.x; 48 | } 49 | return position; 50 | } 51 | 52 | void main() { 53 | vec3 color = vec3(0.025, 0.05, 0.1); 54 | vec2 p = squareFrame(iResolution, gl_FragCoord.xy); 55 | 56 | color = mix(color, red, aastep(0.0, 0.1 - length(p - rt(0.5, 1.0 / 3.0)))); 57 | color = mix(color, green, aastep(0.0, 0.1 - length(p - rt(0.5, 2.0 / 3.0)))); 58 | color = mix(color, blue, aastep(0.0, 0.1 - length(p - rt(0.5, 3.0 / 3.0)))); 59 | 60 | color = mix(color, yellow, aastep(0.0, 0.125 - length(p - rt(0.5, 1.5 / 3.0)))); 61 | color = mix(color, cyan, aastep(0.0, 0.125 - length(p - rt(0.5, 2.5 / 3.0)))); 62 | color = mix(color, magenta, aastep(0.0, 0.125 - length(p - rt(0.5, 3.5 / 3.0)))); 63 | 64 | color = mix(color, white, aastep(0.0, 0.25 - length(p))); 65 | 66 | gl_FragColor = vec4(color, 1); 67 | } 68 | -------------------------------------------------------------------------------- /chapters/03-rgb.md: -------------------------------------------------------------------------------- 1 | # RGBA Color 2 | 3 | Fragment shaders use RGBA color for describing color: each pixel has a Red, Green, Blue and Alpha (opacity) value which when combined can represent any visible colour. 4 | 5 | In GLSL colours are represented as *vectors*: 6 | 7 | * `vec2(brightness, alpha)` 8 | * `vec3(red, green, blue)` 9 | * `vec4(red, green, blue, alpha)` 10 | 11 | For example: 12 | 13 | * Combine *red* and *green* to get *yellow*. 14 | * Combine *green* and *blue* to get *cyan*. 15 | * Combine *red* and *blue* to get *magenta*. 16 | 17 | GLSL vectors are a special type of array where each of the 2–4 values is a number. Vectors in GLSL are first-class citizens, and using them correctly is key to making the most of GLSL's potential. More on that later... 18 | -------------------------------------------------------------------------------- /chapters/04-swizzling.glsl: -------------------------------------------------------------------------------- 1 | #pragma banTokens: vec3 2 | #pragma prefix 3 | uniform vec2 iResolution; 4 | #pragma question 5 | // 6 | // Like before, ensure that the color variables listed 7 | // below match their name. 8 | // 9 | // However, this time use swizzling to do so without 10 | // using `vec3()` constructors anywhere. 11 | // 12 | 13 | vec2 sw = vec2(1, 0); 14 | 15 | vec3 red = vec3(1); 16 | vec3 green = vec3(1); 17 | vec3 blue = vec3(1); 18 | vec3 cyan = vec3(1); 19 | vec3 magenta = vec3(1); 20 | vec3 yellow = vec3(1); 21 | vec3 white = vec3(1); 22 | #pragma solution 23 | vec2 sw = vec2(1, 0); 24 | 25 | vec3 red = sw.xyy; 26 | vec3 green = sw.yxy; 27 | vec3 blue = sw.yyx; 28 | vec3 cyan = sw.yxx; 29 | vec3 magenta = sw.xyx; 30 | vec3 yellow = sw.xxy; 31 | vec3 white = sw.xxx; 32 | #pragma suffix 33 | float aastep(float threshold, float value) { 34 | #ifdef GL_OES_standard_derivatives 35 | float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757; 36 | return smoothstep(threshold-afwidth, threshold+afwidth, value); 37 | #else 38 | return step(threshold, value); 39 | #endif 40 | } 41 | 42 | #define PI 3.14159265359 43 | vec2 rt (float r, float a) { 44 | a *= PI * 2.0; 45 | return r * vec2(sin(a), cos(a)); 46 | } 47 | 48 | vec2 squareFrame(vec2 screenSize, vec2 coord) { 49 | vec2 position = 2.0 * (coord.xy / screenSize.xy) - 1.0; 50 | if (screenSize.x > screenSize.y) { 51 | position.x *= screenSize.x / screenSize.y; 52 | } else { 53 | position.y *= screenSize.y / screenSize.x; 54 | } 55 | return position; 56 | } 57 | 58 | void main() { 59 | vec3 color = vec3(0.025, 0.05, 0.1); 60 | vec2 p = squareFrame(iResolution, gl_FragCoord.xy); 61 | 62 | color = mix(color, red, aastep(0.0, 0.1 - length(p - rt(0.5, 1.0 / 3.0)))); 63 | color = mix(color, green, aastep(0.0, 0.1 - length(p - rt(0.5, 2.0 / 3.0)))); 64 | color = mix(color, blue, aastep(0.0, 0.1 - length(p - rt(0.5, 3.0 / 3.0)))); 65 | 66 | color = mix(color, yellow, aastep(0.0, 0.125 - length(p - rt(0.5, 1.5 / 3.0)))); 67 | color = mix(color, cyan, aastep(0.0, 0.125 - length(p - rt(0.5, 2.5 / 3.0)))); 68 | color = mix(color, magenta, aastep(0.0, 0.125 - length(p - rt(0.5, 3.5 / 3.0)))); 69 | 70 | color = mix(color, white, aastep(0.0, 0.25 - length(p))); 71 | 72 | gl_FragColor = vec4(color, 1); 73 | } 74 | -------------------------------------------------------------------------------- /chapters/04-swizzling.md: -------------------------------------------------------------------------------- 1 | # Swizzling 2 | 3 | Swizzling — beyond being a great name — is a nice feature in GLSL for accessing the properties of a vector. 4 | 5 | You can get a single `float` from a vector using `.r`, `.g`, `.b` or `.a`. For example: 6 | 7 | * `vec4(1, 2, 3, 4).r == 1.0` 8 | * `vec4(1, 2, 3, 4).g == 2.0` 9 | * `vec4(1, 2, 3, 4).b == 3.0` 10 | * `vec4(1, 2, 3, 4).a == 4.0` 11 | 12 | *But* you can also create new vectors from combinations of their components like so: 13 | 14 | * `vec4(1, 2, 3, 4).rb == vec3(1, 3)` 15 | * `vec4(1, 2, 3, 4).rgg == vec3(1, 2, 2)` 16 | * `vec4(1, 2, 3, 4).ggab == vec3(2, 2, 4, 3)` 17 | 18 | In addition to `.rgba`, you can also use `.xyzw`. These are equivalent, but if you're using the vector for a position instead of a color it's easy to reason about when using the latter. 19 | 20 | * `vec4(1, 2, 3, 4).xz == vec3(1, 3)` 21 | * `vec4(1, 2, 3, 4).xyy == vec3(1, 2, 2)` 22 | * `vec4(1, 2, 3, 4).yywz == vec3(2, 2, 4, 3)` 23 | 24 | In this exercise, you can use the `sw` variable to create new colors: 25 | 26 | * `vec3 yellow = sw.xxy;` 27 | 28 | *P.S. don't forget to use semicolons at the end of each line: they're required in GLSL :')* 29 | -------------------------------------------------------------------------------- /chapters/04b-maths-sorry.glsl: -------------------------------------------------------------------------------- 1 | #pragma banTokens: vec2 x y 2 | #pragma question 3 | // 4 | // Get the midpoint of `p1` and `p2` *without* using 5 | // vec2() or any swizzling :O 6 | // 7 | vec2 midpoint(vec2 p1, vec2 p2) { 8 | return vec2(0.0); 9 | } 10 | #pragma solution 11 | vec2 midpoint(vec2 p1, vec2 p2) { 12 | return (p1 + p2) * 0.5; 13 | } 14 | #pragma prefix 15 | uniform float iGlobalTime; 16 | uniform vec2 iResolution; 17 | 18 | float shape_line(vec2 p, vec2 a, vec2 b); 19 | float shape_segment(vec2 p, vec2 a, vec2 b); 20 | float aastep(float threshold, float value); 21 | #pragma suffix 22 | void main() { 23 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution - 1.0; 24 | vec2 p1 = vec2(cos(iGlobalTime * 0.05), sin(iGlobalTime * 0.05)) * 0.9; 25 | vec2 p2 = vec2(cos(iGlobalTime * 0.06), sin(iGlobalTime * 0.06)) * -0.3; 26 | vec2 p3 = midpoint(p1, p2); 27 | 28 | float d = shape_segment(uv, p1, p2) - 0.01; 29 | d = min(d, length(uv - p1) - 0.05); 30 | d = min(d, length(uv - p2) - 0.05); 31 | d = min(d, length(uv - p3) - 0.1); 32 | 33 | float d2 = 5.0; 34 | d2 = min(d2, shape_line(uv, vec2(1, 0), vec2(-1, 0)) - 0.005); 35 | d2 = min(d2, shape_line(uv, vec2(0, 1), vec2(0, -1)) - 0.005); 36 | 37 | vec3 color = vec3(1); 38 | 39 | color -= 1.0 - aastep(0.0, d); 40 | color -= (1.0 - aastep(0.0, d2)) * vec3(0, 0.35, 0.4); 41 | gl_FragColor = vec4(color, 1); 42 | } 43 | 44 | float shape_line(vec2 p, vec2 a, vec2 b) { 45 | vec2 dir = b - a; 46 | return abs(dot(normalize(vec2(dir.y, -dir.x)), a - p)); 47 | } 48 | 49 | float aastep(float threshold, float value) { 50 | #ifdef GL_OES_standard_derivatives 51 | float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757; 52 | return smoothstep(threshold-afwidth, threshold+afwidth, value); 53 | #else 54 | return step(threshold, value); 55 | #endif 56 | } 57 | 58 | float shape_segment(vec2 p, vec2 a, vec2 b) { 59 | float d = shape_line(p, a, b); 60 | float d0 = dot(p - b, b - a); 61 | float d1 = dot(p - a, b - a); 62 | return d1 < 0.0 ? length(a - p) : d0 > 0.0 ? length(b - p) : d; 63 | } 64 | -------------------------------------------------------------------------------- /chapters/04b-maths-sorry.md: -------------------------------------------------------------------------------- 1 | # Maths; Sorry! 2 | 3 | Shader programming leans on a lot of maths to get things done, so much so that most of the code you'll write deals *only* with numbers. 4 | 5 | That's OK though! Often the math is deceptively simple, or you can copy/paste the hard bits from kind folks online such as [Íñigo Quílez](http://www.iquilezles.org/) until it gets easier. Before you know it you'll be running circles around Linear Algebra without even needing to know what Linear Algebra *is*. 6 | 7 | ## Finding the Midpoint 8 | 9 | In addition to colors, vectors can also be used to store *positions*, like we can see in the example on the left. 10 | 11 | The midpoint is just the average of two values, e.g.: 12 | 13 | `(x1 + x2) / 2` 14 | 15 | To calculate the midpoint of a vector, you just have to calculate the midpoint of each of its values: 16 | 17 | `vec2((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0);` 18 | 19 | That's a little verbose though. Can we make it shorter? 20 | 21 | ## Piecewise Operations 22 | 23 | You can treat vectors a little like normal numbers: they can be added, multiplied, divided and subtracted just the same in GLSL! 24 | 25 | * `p1 + p2 == vec2(p1.x + p2.x, p1.y + p2.y)` 26 | * `p1 - p2 == vec2(p1.x - p2.x, p1.y - p2.y)` 27 | * `p1 * p2 == vec2(p1.x * p2.x, p1.y * p2.y)` 28 | * `p1 / p2 == vec2(p1.x / p2.x, p1.y / p2.y)` 29 | 30 | This is called a *piecewise operation*, because it is applied to each *piece* of the vector individually. 31 | 32 | You can even apply a piecewise operation to a vector using `float`, e.g.: 33 | 34 | * `p1 + 1.0 == vec2(p1.x + 1.0, p2.x + 1.0)` 35 | * `p1 - 1.0 == vec2(p1.x - 1.0, p2.x - 1.0)` 36 | * `p1 * 5.0 == vec2(p1.x * 5.0, p2.x * 5.0)` 37 | * `p1 / 5.0 == vec2(p1.x / 5.0, p2.x / 5.0)` 38 | 39 | The rest, I'll leave up to you... 40 | -------------------------------------------------------------------------------- /chapters/05-square.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | uniform vec2 iResolution; 3 | 4 | // 5 | // Let's draw a box in the center of the screen! 6 | // 7 | // It should start where uv == (-0.5, -0.5) and finish 8 | // where uv == (+0.5, +0.5). Change the `isBox` function 9 | // to return `true` when it's within those bounds. 10 | // 11 | bool inBox(vec2 uv) { 12 | return false; 13 | } 14 | 15 | void main() { 16 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 17 | if (inBox(uv)) { 18 | gl_FragColor = vec4(1, 0.6, 0.5, 1); 19 | } else { 20 | gl_FragColor = vec4(0.5, 0.8, 1, 1); 21 | } 22 | } 23 | #pragma solution 24 | uniform vec2 iResolution; 25 | 26 | bool inBox(vec2 uv) { 27 | return uv.x < 0.5 && uv.x > -0.5 && uv.y < 0.5 && uv.y > -0.5; 28 | } 29 | 30 | void main() { 31 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 32 | if (inBox(uv)) { 33 | gl_FragColor = vec4(1, 0.6, 0.5, 1); 34 | } else { 35 | gl_FragColor = vec4(0.5, 0.8, 1, 1); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /chapters/05-square.md: -------------------------------------------------------------------------------- 1 | # Let's Draw a Square 2 | 3 | OK, time to start drawing things proper! 4 | 5 | Fragment shaders are run on each pixel: they're not at all aware of their surrounding pixels. This is a challenging constraint at times, but also why they're so speedy. 6 | 7 | ## Boolean Comparisons 8 | 9 | You can compare `float` values (but not vectors) using run-of-the-mill boolean operators, e.g.: 10 | 11 | * `bool value = uv.x > 1.05;` 12 | * `bool value = uv.x <= 0.0;` 13 | * `bool value = uv.x != 0.5;` 14 | * `bool value = uv.y != 0.0 && uv.x != 0.0;` 15 | * `bool value = uv.y != 0.0 || uv.x != 0.0;` 16 | 17 | ## Scaling to Fit 18 | 19 | Fragment shaders also don't have much to use to find out where they are on the screen either. `gl_FragCoord.xy` will give you the exact position in pixels, but if you resize the screen the size of the object won't change to fit it. 20 | 21 | So we pass in a *uniform* value that contains the size, or resolution, of the screen. A uniform is a value passed in from JavaScript that is the same for every pixel in the shader. It's useful for giving the fragment shader some context to work with: for example, you might also pass in the time in seconds to animate the output. 22 | 23 | By dividing `gl_FragCoord.xy` by `iResolution`, we can get a value between 0 and 1 for the pixel's position on the screen: 24 | 25 | `vec2 p = gl_FragCoord.xy / iResolution;` 26 | 27 | Note that in our example we've scaled it slightly differently: the top-left is `vec2(-1, -1)` and the bottom-right is `vec2(+1, +1)`. 28 | -------------------------------------------------------------------------------- /chapters/06-circle.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | uniform vec2 iResolution; 3 | uniform float iGlobalTime; 4 | 5 | // 6 | // Let's draw a circle in the center of the screen! 7 | // 8 | // Its center should be at uv == (0, 0), and its `radius` 9 | // should match the value passed into `inCircle`. 10 | // 11 | bool inCircle(vec2 uv, float radius) { 12 | return false; 13 | } 14 | 15 | // If it's starting to look like there's a lot going on in 16 | // main(), don't worry! You don't need to change it, it's 17 | // just doing some heavy lifting for you. Leaving it in 18 | // here in case it's helpful for you :) 19 | void main() { 20 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 21 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.5 + 0.3; 22 | if (inCircle(uv, radius)) { 23 | gl_FragColor = vec4(1, 0.6, 0.5, 1); 24 | } else { 25 | gl_FragColor = vec4(0.5, 0.8, 1, 1); 26 | } 27 | } 28 | #pragma solution 29 | uniform vec2 iResolution; 30 | uniform float iGlobalTime; 31 | 32 | bool inCircle(vec2 uv, float radius) { 33 | return length(uv) < radius; 34 | } 35 | 36 | void main() { 37 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 38 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.5 + 0.3; 39 | if (inCircle(uv, radius)) { 40 | gl_FragColor = vec4(1, 0.6, 0.5, 1); 41 | } else { 42 | gl_FragColor = vec4(0.5, 0.8, 1, 1); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chapters/06-circle.md: -------------------------------------------------------------------------------- 1 | # Vectors and Builtins 2 | 3 | OK, we've drawn a square — next, a circle. 4 | 5 | You can check if a point is in a circle by seeing if its `distance` from the circle's center is smaller than the circle's `radius`. In GLSL we measure distance using the `length()` function: 6 | 7 | `float d = length(p2 - p1)` 8 | 9 | Much more concise than it would be in JavaScript! 10 | 11 | All that's left to do is compare that distance to the radius and you should be able to draw that circle out to the screen. Good luck :) 12 | -------------------------------------------------------------------------------- /chapters/07-distance-fields.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | uniform vec2 iResolution; 3 | uniform float iGlobalTime; 4 | 5 | // 6 | // Let's make a distance function! 7 | // 8 | // We'll start with a circle. Its center should be at 9 | // `point` == (0, 0), and its `radius` should match the value 10 | // passed into `distanceFromCircle`. 11 | // 12 | float distanceFromCircle(vec2 point, float radius) { 13 | return 0.0; 14 | } 15 | 16 | void main() { 17 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 18 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.5 + 0.3; 19 | float dist = distanceFromCircle(uv, radius); 20 | 21 | // draw_distance is a little utility we're using from 22 | // https://www.shadertoy.com/view/XsyGRW 23 | // 24 | // It'll draw out your distance function for you, showing 25 | // negative distances in a dark orange and positive distances 26 | // in yellow -> blue. 27 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 28 | } 29 | #pragma solution 30 | uniform vec2 iResolution; 31 | uniform float iGlobalTime; 32 | 33 | float distanceFromCircle(vec2 point, float radius) { 34 | return length(point) - radius; 35 | } 36 | 37 | void main() { 38 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 39 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.5 + 0.3; 40 | float dist = distanceFromCircle(uv, radius); 41 | 42 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 43 | } 44 | #pragma prefix 45 | vec3 draw_line(float d); 46 | float draw_solid(float d); 47 | vec3 draw_distance(float d, vec2 p); 48 | #pragma suffix 49 | vec3 draw_line(float d) { 50 | const float aa = 3.0; 51 | const float thickness = 0.0025; 52 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 53 | } 54 | 55 | float draw_solid(float d) { 56 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 57 | } 58 | 59 | vec3 draw_distance(float d, vec2 p) { 60 | float t = clamp(d * 0.85, 0.0, 1.0); 61 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 62 | 63 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 64 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 65 | float d2 = abs(1.0 - draw_line(d).x); 66 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 67 | 68 | grad -= rim; 69 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 70 | 71 | return grad; 72 | } 73 | -------------------------------------------------------------------------------- /chapters/07-distance-fields.md: -------------------------------------------------------------------------------- 1 | # Distance Functions 2 | 3 | Drawing out shapes with booleans is all well and good, but it's not very flexible. There's another technique which allows us a whole bunch of flexibility, and it's the trick behind most of the demos you'll see on [Shadertoy](https://shadertoy.com/). 4 | 5 | ## Signed Distance Functions 6 | 7 | Yep, those words probably make a lot less sense when they're put together like that. 8 | 9 | To start: a **distance function** takes a point as input and returns the distance from a surface as output. In GLSL, you'd mark up a 2D Distance Function like so: 10 | 11 | `float distanceFn(vec2 position);` 12 | 13 | A **signed distance function** (SDF) is very similar, but it returns a *negative* value when it's inside the surface. You can now very quickly draw out that shape in 2D by checking if the SDF's value is less than zero. 14 | 15 | Here's an SDF for a point at `(0, 0)`: 16 | 17 | `length(position);` 18 | 19 | You can subtract values from the SDF to make it grow outwards. This way you can make a circle at `(0, 0)`: 20 | 21 | `length(position) - 1.0;` 22 | 23 | Try it out with different values in the exercise! 24 | -------------------------------------------------------------------------------- /chapters/08-moving-shapes.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | uniform vec2 iResolution; 3 | uniform float iGlobalTime; 4 | 5 | // 6 | // Time to move the distance field away from the center. 7 | // 8 | // Place the circle such that its center is at `origin` 9 | // every frame. 10 | // 11 | float distanceField(vec2 point, vec2 origin, float radius) { 12 | return length(point) - radius; 13 | } 14 | 15 | void main() { 16 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 17 | vec2 origin = vec2(sin(iGlobalTime * 0.3) * 0.3); 18 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.5 + 0.3; 19 | float dist = distanceField(uv, origin, radius); 20 | 21 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 22 | } 23 | #pragma solution 24 | uniform vec2 iResolution; 25 | uniform float iGlobalTime; 26 | 27 | float distanceField(vec2 point, vec2 origin, float radius) { 28 | return length(point - origin) - radius; 29 | } 30 | 31 | void main() { 32 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 33 | vec2 origin = vec2(sin(iGlobalTime * 0.3) * 0.3); 34 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.5 + 0.3; 35 | float dist = distanceField(uv, origin, radius); 36 | 37 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 38 | } 39 | #pragma prefix 40 | vec3 draw_line(float d); 41 | float draw_solid(float d); 42 | vec3 draw_distance(float d, vec2 p); 43 | #pragma suffix 44 | vec3 draw_line(float d) { 45 | const float aa = 3.0; 46 | const float thickness = 0.0025; 47 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 48 | } 49 | 50 | float draw_solid(float d) { 51 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 52 | } 53 | 54 | vec3 draw_distance(float d, vec2 p) { 55 | float t = clamp(d * 0.85, 0.0, 1.0); 56 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 57 | 58 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 59 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 60 | float d2 = abs(1.0 - draw_line(d).x); 61 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 62 | 63 | grad -= rim; 64 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 65 | 66 | return grad; 67 | } 68 | -------------------------------------------------------------------------------- /chapters/08-moving-shapes.md: -------------------------------------------------------------------------------- 1 | # Moving Shapes 2 | 3 | There's a whole collection of tricks you can use to manipulate SDFs and create complex geometry. We'll start out simple, and try moving our circle around the screen. 4 | 5 | *Hint: you might want to check back to a [previous exercise](06-circle.html).* 6 | -------------------------------------------------------------------------------- /chapters/09-combining-shapes.glsl: -------------------------------------------------------------------------------- 1 | #pragma prefix 2 | vec3 draw_line(float d); 3 | float draw_solid(float d); 4 | vec3 draw_distance(float d, vec2 p); 5 | #pragma question 6 | uniform vec2 iResolution; 7 | uniform float iGlobalTime; 8 | 9 | // 10 | // Get the distance field of two circles together in 11 | // a single function. 12 | // 13 | float distanceField(vec2 point, vec2 origin1, vec2 origin2, float radius) { 14 | float d1 = length(point - origin1) - radius; 15 | float d2 = length(point - origin2) - radius; 16 | 17 | return 0.0; 18 | } 19 | 20 | void main() { 21 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 22 | vec2 origin = vec2(sin(iGlobalTime * 0.3) * 0.3); 23 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.3 + 0.05; 24 | float dist = distanceField(uv, origin, -origin, radius); 25 | 26 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 27 | } 28 | #pragma solution 29 | uniform vec2 iResolution; 30 | uniform float iGlobalTime; 31 | 32 | float distanceField(vec2 point, vec2 origin1, vec2 origin2, float radius) { 33 | float d1 = length(point - origin1) - radius; 34 | float d2 = length(point - origin2) - radius; 35 | return min(d1, d2); 36 | } 37 | 38 | void main() { 39 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 40 | vec2 origin = vec2(sin(iGlobalTime * 0.3) * 0.3); 41 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.3 + 0.05; 42 | float dist = distanceField(uv, origin, -origin, radius); 43 | 44 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 45 | } 46 | #pragma suffix 47 | vec3 draw_line(float d) { 48 | const float aa = 3.0; 49 | const float thickness = 0.0025; 50 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 51 | } 52 | 53 | float draw_solid(float d) { 54 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 55 | } 56 | 57 | vec3 draw_distance(float d, vec2 p) { 58 | float t = clamp(d * 0.85, 0.0, 1.0); 59 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 60 | 61 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 62 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 63 | float d2 = abs(1.0 - draw_line(d).x); 64 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 65 | 66 | grad -= rim; 67 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 68 | 69 | return grad; 70 | } 71 | -------------------------------------------------------------------------------- /chapters/09-combining-shapes.md: -------------------------------------------------------------------------------- 1 | # Combining Shapes 2 | 3 | OK, now we're going to combine two shapes into one! This is called a *union*. The cool thing about distance fields is that we only need to work with the distance here: you're just finding the closer of the two distances. 4 | 5 | You can do this with a single one of GLSL's builtin functions. 6 | -------------------------------------------------------------------------------- /chapters/09b-blending-shapes.glsl: -------------------------------------------------------------------------------- 1 | #pragma prefix 2 | vec3 draw_line(float d); 3 | float draw_solid(float d); 4 | vec3 draw_distance(float d, vec2 p); 5 | #pragma question 6 | uniform vec2 iResolution; 7 | uniform float iGlobalTime; 8 | 9 | // 10 | // Blend two circles together using a polynomial smooth minimum, 11 | // using a "smoothness" of 0.1 12 | // 13 | float distanceField(vec2 point, vec2 origin1, vec2 origin2, float radius) { 14 | float d1 = length(point - origin1) - radius; 15 | float d2 = length(point - origin2) - radius; 16 | return min(d1, d2); 17 | } 18 | 19 | void main() { 20 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 21 | vec2 origin = vec2(sin(iGlobalTime * 0.11) * 0.3); 22 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.3 + 0.05; 23 | float dist = distanceField(uv, origin, -origin, radius); 24 | 25 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 26 | } 27 | #pragma solution 28 | uniform vec2 iResolution; 29 | uniform float iGlobalTime; 30 | 31 | float smin(float a, float b, float k) { 32 | float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); 33 | return mix(b, a, h) - k * h * (1.0 - h); 34 | } 35 | 36 | float distanceField(vec2 point, vec2 origin1, vec2 origin2, float radius) { 37 | float d1 = length(point - origin1) - radius; 38 | float d2 = length(point - origin2) - radius; 39 | return smin(d1, d2, 0.1); 40 | } 41 | 42 | void main() { 43 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 44 | vec2 origin = vec2(sin(iGlobalTime * 0.11) * 0.3); 45 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.3 + 0.05; 46 | float dist = distanceField(uv, origin, -origin, radius); 47 | 48 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 49 | } 50 | #pragma suffix 51 | vec3 draw_line(float d) { 52 | const float aa = 3.0; 53 | const float thickness = 0.0025; 54 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 55 | } 56 | 57 | float draw_solid(float d) { 58 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 59 | } 60 | 61 | vec3 draw_distance(float d, vec2 p) { 62 | float t = clamp(d * 0.85, 0.0, 1.0); 63 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 64 | 65 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 66 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 67 | float d2 = abs(1.0 - draw_line(d).x); 68 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 69 | 70 | grad -= rim; 71 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 72 | 73 | return grad; 74 | } 75 | -------------------------------------------------------------------------------- /chapters/09b-blending-shapes.md: -------------------------------------------------------------------------------- 1 | # Blending Shapes 2 | 3 | Combining shapes using `min()` was a good start, but there's nicer approaches available to us as well. 4 | 5 | ## Smooth Minimum 6 | 7 | Distance Functions have a lot of mathematical theory behind them: they're a type of [Implicit Function](https://en.wikipedia.org/wiki/Implicit_function). The maths go deep, but we can cherrypick useful tricks here and there from the theory. One such example is the **smooth minimum**. 8 | 9 | A smooth minimum function takes two values as input, and returns a smoothed out value between the two. There's different types of smooth minimum with different tradeoffs, but here's a few taken from the ever-useful [iquilezles.org](http://iquilezles.org/www/articles/smin/smin.htm): 10 | 11 | ### Exponential 12 | 13 | ``` 14 | float smin( float a, float b, float k ) 15 | { 16 | float res = exp( -k*a ) + exp( -k*b ); 17 | return -log( res )/k; 18 | } 19 | ``` 20 | 21 | ### Polynomial 22 | 23 | ``` 24 | float smin( float a, float b, float k ) 25 | { 26 | float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 ); 27 | return mix( b, a, h ) - k*h*(1.0-h); 28 | } 29 | ``` 30 | 31 | ### Power 32 | 33 | ``` 34 | float smin( float a, float b, float k ) 35 | { 36 | a = pow( a, k ); b = pow( b, k ); 37 | return pow( (a*b)/(a+b), 1.0/k ); 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /chapters/10-repeating-space.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | uniform vec2 iResolution; 3 | uniform float iGlobalTime; 4 | 5 | // 6 | // Modify `point` so that we get circles filling the screen. 7 | // There should be one circle in each direction every 0.5 units, 8 | // and the original circle should remain in the center of the screen. 9 | // 10 | float distanceFromCircle(vec2 point, float radius) { 11 | return length(point) - radius; 12 | } 13 | 14 | void main() { 15 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 16 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.1 + 0.05; 17 | float dist = distanceFromCircle(uv, radius); 18 | 19 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 20 | } 21 | #pragma solution 22 | uniform vec2 iResolution; 23 | uniform float iGlobalTime; 24 | 25 | float distanceFromCircle(vec2 point, float radius) { 26 | point = mod(point + 0.25, 0.5) - 0.25; 27 | return length(point) - radius; 28 | } 29 | 30 | void main() { 31 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 32 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.1 + 0.05; 33 | float dist = distanceFromCircle(uv, radius); 34 | 35 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 36 | } 37 | #pragma prefix 38 | vec3 draw_line(float d); 39 | float draw_solid(float d); 40 | vec3 draw_distance(float d, vec2 p); 41 | #pragma suffix 42 | vec3 draw_line(float d) { 43 | const float aa = 3.0; 44 | const float thickness = 0.0025; 45 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 46 | } 47 | 48 | float draw_solid(float d) { 49 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 50 | } 51 | 52 | vec3 draw_distance(float d, vec2 p) { 53 | float t = clamp(d * 0.85, 0.0, 1.0); 54 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 55 | 56 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 57 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 58 | float d2 = abs(1.0 - draw_line(d).x); 59 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 60 | 61 | grad -= rim; 62 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 63 | 64 | return grad; 65 | } 66 | -------------------------------------------------------------------------------- /chapters/10-repeating-space.md: -------------------------------------------------------------------------------- 1 | # Repeating Space 2 | 3 | Because an SDF just takes a point as input, you can manipulate space around your object to get unlimited repetition more or less for free! 4 | 5 | This is called *domain repetition*, and is achieved using GLSL's `mod()` function. 6 | -------------------------------------------------------------------------------- /chapters/10b-primitives.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | uniform vec2 iResolution; 3 | uniform float iGlobalTime; 4 | 5 | // 6 | // Switch out the circle for a hexagon primitive 7 | // of the same `radius`. 8 | // 9 | float distanceFromCircle(vec2 point, float radius) { 10 | point = mod(point + 0.25, 0.5) - 0.25; 11 | return length(point) - radius; 12 | } 13 | 14 | void main() { 15 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 16 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.1 + 0.05; 17 | float dist = distanceFromCircle(uv, radius); 18 | 19 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 20 | } 21 | #pragma solution 22 | uniform vec2 iResolution; 23 | uniform float iGlobalTime; 24 | 25 | float distanceFromCircle(vec2 point, float radius) { 26 | point = mod(point + 0.25, 0.5) - 0.25; 27 | vec2 q = abs(point); 28 | return max((q.x * 0.866025 + q.y * 0.5), q.y) - radius; 29 | } 30 | 31 | void main() { 32 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution.xy - 1.0; 33 | float radius = (sin(iGlobalTime * 0.25) * 0.5 + 0.5) * 0.1 + 0.05; 34 | float dist = distanceFromCircle(uv, radius); 35 | 36 | gl_FragColor = vec4(draw_distance(dist, uv), 1); 37 | } 38 | #pragma prefix 39 | vec3 draw_line(float d); 40 | float draw_solid(float d); 41 | vec3 draw_distance(float d, vec2 p); 42 | #pragma suffix 43 | vec3 draw_line(float d) { 44 | const float aa = 3.0; 45 | const float thickness = 0.0025; 46 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 47 | } 48 | 49 | float draw_solid(float d) { 50 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 51 | } 52 | 53 | vec3 draw_distance(float d, vec2 p) { 54 | float t = clamp(d * 0.85, 0.0, 1.0); 55 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 56 | 57 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 58 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 59 | float d2 = abs(1.0 - draw_line(d).x); 60 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 61 | 62 | grad -= rim; 63 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 64 | 65 | return grad; 66 | } 67 | -------------------------------------------------------------------------------- /chapters/10b-primitives.md: -------------------------------------------------------------------------------- 1 | # Primitives 2 | 3 | Of course, all of the techniques we've just covered are not limited to circles. There's a number of primitives you can use instead! Here's a few examples: 4 | 5 | ### Circle 6 | 7 | ``` glsl 8 | float circle(vec2 point, float radius) { 9 | return length(point) - radius; 10 | } 11 | ``` 12 | 13 | ### Box 14 | 15 | ``` glsl 16 | float box(vec2 point, vec2 size) { 17 | vec2 d = abs(point) - size; 18 | return min(max(d.x,d.y),0.0) + length(max(d,0.0)); 19 | } 20 | ``` 21 | 22 | ### Hexagon 23 | 24 | ``` glsl 25 | float hexagon(vec2 point, float radius) { 26 | vec2 q = abs(point); 27 | return max((q.x * 0.866025 + q.y * 0.5), q.y) - radius; 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /chapters/11-add-a-dimension.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | // 3 | // In this example, the sphere's origin is (0, 0, 0). 4 | // 5 | // Change it so that the sphere resets directly on top of 6 | // (0, 0, 0) based on the radius. 7 | // 8 | float getDistanceFromPoint(vec3 point) { 9 | float radius = (sin(iGlobalTime * 0.1) * 0.5 + 0.5) * 0.25; 10 | 11 | return length(point) - radius; 12 | } 13 | #pragma solution 14 | float getDistanceFromPoint(vec3 point) { 15 | float radius = (sin(iGlobalTime * 0.1) * 0.5 + 0.5) * 0.25; 16 | return length(point - vec3(0, radius, 0)) - radius; 17 | } 18 | #pragma prefix 19 | uniform vec2 iResolution; 20 | uniform float iGlobalTime; 21 | 22 | float getDistanceFromPoint(vec3 point); 23 | vec3 draw_line(float d); 24 | float draw_solid(float d); 25 | vec3 draw_distance(float d, vec2 p); 26 | 27 | mat3 calcLookAtMatrix(vec3 origin, vec3 target, float roll) { 28 | vec3 rr = vec3(sin(roll), cos(roll), 0.0); 29 | vec3 ww = normalize(target - origin); 30 | vec3 uu = normalize(cross(ww, rr)); 31 | vec3 vv = normalize(cross(uu, ww)); 32 | 33 | return mat3(uu, vv, ww); 34 | } 35 | 36 | vec3 calcNormal(vec3 pos, float eps) { 37 | const vec3 v1 = vec3( 1.0,-1.0,-1.0); 38 | const vec3 v2 = vec3(-1.0,-1.0, 1.0); 39 | const vec3 v3 = vec3(-1.0, 1.0,-1.0); 40 | const vec3 v4 = vec3( 1.0, 1.0, 1.0); 41 | 42 | return normalize( v1 * getDistanceFromPoint( pos + v1*eps ) + 43 | v2 * getDistanceFromPoint( pos + v2*eps ) + 44 | v3 * getDistanceFromPoint( pos + v3*eps ) + 45 | v4 * getDistanceFromPoint( pos + v4*eps ) ); 46 | } 47 | 48 | vec3 getRay(vec3 origin, vec3 target, vec2 screenPos, float lensLength) { 49 | mat3 camMat = calcLookAtMatrix(origin, target, 0.0); 50 | return normalize(camMat * vec3(screenPos, lensLength)); 51 | } 52 | 53 | float intersectPlane(vec3 ro, vec3 rd, vec3 nor, float dist) { 54 | float denom = dot(rd, nor); 55 | float t = -(dot(ro, nor) + dist) / denom; 56 | 57 | return t; 58 | } 59 | #pragma suffix 60 | float beckmannDistribution(float x, float roughness) { 61 | float NdotH = max(x, 0.0001); 62 | float cos2Alpha = NdotH * NdotH; 63 | float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha; 64 | float roughness2 = roughness * roughness; 65 | float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha; 66 | return exp(tan2Alpha / roughness2) / denom; 67 | } 68 | 69 | float cookTorranceSpecular( 70 | vec3 lightDirection, 71 | vec3 viewDirection, 72 | vec3 surfaceNormal, 73 | float roughness, 74 | float fresnel) { 75 | 76 | float VdotN = max(dot(viewDirection, surfaceNormal), 0.0); 77 | float LdotN = max(dot(lightDirection, surfaceNormal), 0.0); 78 | 79 | //Half angle vector 80 | vec3 H = normalize(lightDirection + viewDirection); 81 | 82 | //Geometric term 83 | float NdotH = max(dot(surfaceNormal, H), 0.0); 84 | float VdotH = max(dot(viewDirection, H), 0.000001); 85 | float x = 2.0 * NdotH / VdotH; 86 | float G = min(1.0, min(x * VdotN, x * LdotN)); 87 | 88 | //Distribution term 89 | float D = beckmannDistribution(NdotH, roughness); 90 | 91 | //Fresnel term 92 | float F = pow(1.0 - VdotN, fresnel); 93 | 94 | //Multiply terms and done 95 | return G * F * D / max(3.14159265 * VdotN * LdotN, 0.000001); 96 | } 97 | 98 | void main() { 99 | float time = iGlobalTime * 0.025; 100 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution - 1.0; 101 | vec3 ro = vec3(sin(time), 1.0, cos(time)); 102 | vec3 ta = vec3(0); 103 | vec3 rd = getRay(ro, ta, uv, 2.0); 104 | 105 | float t = -1.0; 106 | float mind = 0.01; 107 | float maxd = 10.0; 108 | float latest = 1.0; 109 | for (int i = 0; i < 30; i++) { 110 | if (latest < mind || t > maxd) break; 111 | t += (latest = getDistanceFromPoint(ro + rd * t)); 112 | } 113 | 114 | float tPlane = intersectPlane(ro, rd, vec3(0, 1, 0), 0.0); 115 | 116 | if (tPlane > -0.5 && tPlane < t) { 117 | vec3 pos = ro + rd * tPlane; 118 | gl_FragColor = vec4(draw_distance(getDistanceFromPoint(pos) - 0.0125, pos.xz), 1); 119 | } else 120 | if (t > maxd) { 121 | gl_FragColor = vec4(0, 0, 0, 1); 122 | } else { 123 | vec3 pos = ro + rd * t; 124 | vec3 normal = calcNormal(pos, 0.002); 125 | vec3 ldir = normalize(vec3(0, 1, 0.2)); 126 | float mag = max(0.2, dot(normal, ldir)); 127 | 128 | mag = pow(mag, 0.3545); 129 | mag *= 1.75; 130 | //mag = 0.0; 131 | 132 | gl_FragColor = vec4(mag * vec3(0.95, 0.45, 0.15), 1); 133 | gl_FragColor.rgb += cookTorranceSpecular(ldir, -rd, normal, 1.0, 3.25) * 1.5; 134 | } 135 | } 136 | 137 | vec3 draw_line(float d) { 138 | const float aa = 3.0; 139 | const float thickness = 0.0025; 140 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 141 | } 142 | 143 | float draw_solid(float d) { 144 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 145 | } 146 | 147 | vec3 draw_distance(float d, vec2 p) { 148 | float t = clamp(d * 0.85, 0.0, 1.0); 149 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 150 | 151 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 152 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 153 | float d2 = abs(1.0 - draw_line(d).x); 154 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 155 | 156 | grad -= rim * clamp(1.25 - d, 0.0, 1.0); 157 | grad -= 1.0 - clamp(1.25 - d * 0.25, 0.0, 1.0); 158 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 159 | 160 | return grad; 161 | } 162 | -------------------------------------------------------------------------------- /chapters/11-add-a-dimension.md: -------------------------------------------------------------------------------- 1 | # Let's Add a Dimension 2 | 3 | Another cool thing about distance functions: everything you've learn so far generalises to 3D! Using a technique called [raymarching](http://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/) we can draw out a 3D distance function entirely in a fragment shader. 4 | 5 | We've hidden the code that does this because it can be a lot to take on at once, but you can find a bunch of examples online. To name a few: 6 | 7 | * [cabbibo's SDF Tutorial](https://www.shadertoy.com/view/Xl2XWt) 8 | * [Shadertoy Template — 3D](https://www.shadertoy.com/view/ldfSWs) 9 | * [2D SDF Toy](https://www.shadertoy.com/view/XsyGRW) 10 | -------------------------------------------------------------------------------- /chapters/12-combining-again.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | // 3 | // Using one of the techniques you learnt previously, combine 4 | // `d1`, `d2`, `d3` and `d4` into a single 3D shape. 5 | // 6 | float getDistanceFromPoint(vec3 point) { 7 | vec3 offset = vec3(0.25, 0, 0); 8 | float radius = 0.3; 9 | float d1 = length(point - offset) - radius; 10 | float d2 = length(point + offset) - radius; 11 | float d3 = length(point - offset.zyx) - radius; 12 | float d4 = length(point + offset.zyx) - radius; 13 | 14 | return d1; 15 | } 16 | #pragma solution 17 | float getDistanceFromPoint(vec3 point) { 18 | vec3 offset = vec3(0.25, 0, 0); 19 | float radius = 0.3; 20 | float d1 = length(point - offset) - radius; 21 | float d2 = length(point + offset) - radius; 22 | float d3 = length(point - offset.zyx) - radius; 23 | float d4 = length(point + offset.zyx) - radius; 24 | 25 | return min(min(d1, d2), min(d3, d4)); 26 | } 27 | #pragma prefix 28 | uniform vec2 iResolution; 29 | uniform float iGlobalTime; 30 | 31 | float getDistanceFromPoint(vec3 point); 32 | vec3 draw_line(float d); 33 | float draw_solid(float d); 34 | vec3 draw_distance(float d, vec2 p); 35 | 36 | mat3 calcLookAtMatrix(vec3 origin, vec3 target, float roll) { 37 | vec3 rr = vec3(sin(roll), cos(roll), 0.0); 38 | vec3 ww = normalize(target - origin); 39 | vec3 uu = normalize(cross(ww, rr)); 40 | vec3 vv = normalize(cross(uu, ww)); 41 | 42 | return mat3(uu, vv, ww); 43 | } 44 | 45 | vec3 calcNormal(vec3 pos, float eps) { 46 | const vec3 v1 = vec3( 1.0,-1.0,-1.0); 47 | const vec3 v2 = vec3(-1.0,-1.0, 1.0); 48 | const vec3 v3 = vec3(-1.0, 1.0,-1.0); 49 | const vec3 v4 = vec3( 1.0, 1.0, 1.0); 50 | 51 | return normalize( v1 * getDistanceFromPoint( pos + v1*eps ) + 52 | v2 * getDistanceFromPoint( pos + v2*eps ) + 53 | v3 * getDistanceFromPoint( pos + v3*eps ) + 54 | v4 * getDistanceFromPoint( pos + v4*eps ) ); 55 | } 56 | 57 | vec3 getRay(vec3 origin, vec3 target, vec2 screenPos, float lensLength) { 58 | mat3 camMat = calcLookAtMatrix(origin, target, 0.0); 59 | return normalize(camMat * vec3(screenPos, lensLength)); 60 | } 61 | 62 | float intersectPlane(vec3 ro, vec3 rd, vec3 nor, float dist) { 63 | float denom = dot(rd, nor); 64 | float t = -(dot(ro, nor) + dist) / denom; 65 | 66 | return t; 67 | } 68 | #pragma suffix 69 | float beckmannDistribution(float x, float roughness) { 70 | float NdotH = max(x, 0.0001); 71 | float cos2Alpha = NdotH * NdotH; 72 | float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha; 73 | float roughness2 = roughness * roughness; 74 | float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha; 75 | return exp(tan2Alpha / roughness2) / denom; 76 | } 77 | 78 | float cookTorranceSpecular( 79 | vec3 lightDirection, 80 | vec3 viewDirection, 81 | vec3 surfaceNormal, 82 | float roughness, 83 | float fresnel) { 84 | 85 | float VdotN = max(dot(viewDirection, surfaceNormal), 0.0); 86 | float LdotN = max(dot(lightDirection, surfaceNormal), 0.0); 87 | 88 | //Half angle vector 89 | vec3 H = normalize(lightDirection + viewDirection); 90 | 91 | //Geometric term 92 | float NdotH = max(dot(surfaceNormal, H), 0.0); 93 | float VdotH = max(dot(viewDirection, H), 0.000001); 94 | float x = 2.0 * NdotH / VdotH; 95 | float G = min(1.0, min(x * VdotN, x * LdotN)); 96 | 97 | //Distribution term 98 | float D = beckmannDistribution(NdotH, roughness); 99 | 100 | //Fresnel term 101 | float F = pow(1.0 - VdotN, fresnel); 102 | 103 | //Multiply terms and done 104 | return G * F * D / max(3.14159265 * VdotN * LdotN, 0.000001); 105 | } 106 | 107 | void main() { 108 | float time = iGlobalTime * 0.025; 109 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution - 1.0; 110 | vec3 ro = vec3(sin(time), 1.0, cos(time)); 111 | vec3 ta = vec3(0); 112 | vec3 rd = getRay(ro, ta, uv, 2.0); 113 | 114 | float t = -1.0; 115 | float mind = 0.01; 116 | float maxd = 10.0; 117 | float latest = 1.0; 118 | for (int i = 0; i < 30; i++) { 119 | if (latest < mind || t > maxd) break; 120 | t += (latest = getDistanceFromPoint(ro + rd * t)); 121 | } 122 | 123 | float tPlane = intersectPlane(ro, rd, vec3(0, 1, 0), 0.0); 124 | 125 | if (tPlane > -0.5 && tPlane < t) { 126 | vec3 pos = ro + rd * tPlane; 127 | gl_FragColor = vec4(draw_distance(getDistanceFromPoint(pos) - 0.0125, pos.xz), 1); 128 | } else 129 | if (t > maxd) { 130 | gl_FragColor = vec4(0, 0, 0, 1); 131 | } else { 132 | vec3 pos = ro + rd * t; 133 | vec3 normal = calcNormal(pos, 0.002); 134 | vec3 ldir = normalize(vec3(0, 1, 0.2)); 135 | float mag = max(0.2, dot(normal, ldir)); 136 | 137 | mag = pow(mag, 0.3545); 138 | mag *= 1.75; 139 | //mag = 0.0; 140 | 141 | gl_FragColor = vec4(mag * vec3(0.95, 0.45, 0.15), 1); 142 | gl_FragColor.rgb += cookTorranceSpecular(ldir, -rd, normal, 1.0, 3.25) * 1.5; 143 | } 144 | } 145 | 146 | vec3 draw_line(float d) { 147 | const float aa = 3.0; 148 | const float thickness = 0.0025; 149 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 150 | } 151 | 152 | float draw_solid(float d) { 153 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 154 | } 155 | 156 | vec3 draw_distance(float d, vec2 p) { 157 | float t = clamp(d * 0.85, 0.0, 1.0); 158 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 159 | 160 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 161 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 162 | float d2 = abs(1.0 - draw_line(d).x); 163 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 164 | 165 | grad -= rim * clamp(1.25 - d, 0.0, 1.0); 166 | grad -= 1.0 - clamp(1.25 - d * 0.25, 0.0, 1.0); 167 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 168 | 169 | return grad; 170 | } 171 | -------------------------------------------------------------------------------- /chapters/12-combining-again.md: -------------------------------------------------------------------------------- 1 | # Combining Shapes II 2 | 3 | Remember: all the [tricks](09-combining-shapes.html) you've learnt so far in 2D also apply here! 4 | -------------------------------------------------------------------------------- /chapters/13-blending-again.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | // 3 | // Use a polynomial smooth minimum (k = 0.1) to combine `d1` 4 | // and `d2`. 5 | // 6 | float getDistanceFromPoint(vec3 point) { 7 | vec3 offset = vec3(0.25 * sin(iGlobalTime * 0.15), 0, 0); 8 | float d1 = length(point - offset) - 0.1; 9 | float d2 = length(point + offset) - 0.3; 10 | 11 | return min(d1, d2); 12 | } 13 | #pragma solution 14 | float smin( float a, float b, float k ) 15 | { 16 | float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 ); 17 | return mix( b, a, h ) - k*h*(1.0-h); 18 | } 19 | float getDistanceFromPoint(vec3 point) { 20 | vec3 offset = vec3(0.25 * sin(iGlobalTime * 0.15), 0, 0); 21 | float d1 = length(point - offset) - 0.1; 22 | float d2 = length(point + offset) - 0.3; 23 | 24 | return smin(d1, d2, 0.1); 25 | } 26 | #pragma prefix 27 | uniform vec2 iResolution; 28 | uniform float iGlobalTime; 29 | 30 | float getDistanceFromPoint(vec3 point); 31 | vec3 draw_line(float d); 32 | float draw_solid(float d); 33 | vec3 draw_distance(float d, vec2 p); 34 | 35 | mat3 calcLookAtMatrix(vec3 origin, vec3 target, float roll) { 36 | vec3 rr = vec3(sin(roll), cos(roll), 0.0); 37 | vec3 ww = normalize(target - origin); 38 | vec3 uu = normalize(cross(ww, rr)); 39 | vec3 vv = normalize(cross(uu, ww)); 40 | 41 | return mat3(uu, vv, ww); 42 | } 43 | 44 | vec3 calcNormal(vec3 pos, float eps) { 45 | const vec3 v1 = vec3( 1.0,-1.0,-1.0); 46 | const vec3 v2 = vec3(-1.0,-1.0, 1.0); 47 | const vec3 v3 = vec3(-1.0, 1.0,-1.0); 48 | const vec3 v4 = vec3( 1.0, 1.0, 1.0); 49 | 50 | return normalize( v1 * getDistanceFromPoint( pos + v1*eps ) + 51 | v2 * getDistanceFromPoint( pos + v2*eps ) + 52 | v3 * getDistanceFromPoint( pos + v3*eps ) + 53 | v4 * getDistanceFromPoint( pos + v4*eps ) ); 54 | } 55 | 56 | vec3 getRay(vec3 origin, vec3 target, vec2 screenPos, float lensLength) { 57 | mat3 camMat = calcLookAtMatrix(origin, target, 0.0); 58 | return normalize(camMat * vec3(screenPos, lensLength)); 59 | } 60 | 61 | float intersectPlane(vec3 ro, vec3 rd, vec3 nor, float dist) { 62 | float denom = dot(rd, nor); 63 | float t = -(dot(ro, nor) + dist) / denom; 64 | 65 | return t; 66 | } 67 | #pragma suffix 68 | float beckmannDistribution(float x, float roughness) { 69 | float NdotH = max(x, 0.0001); 70 | float cos2Alpha = NdotH * NdotH; 71 | float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha; 72 | float roughness2 = roughness * roughness; 73 | float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha; 74 | return exp(tan2Alpha / roughness2) / denom; 75 | } 76 | 77 | float cookTorranceSpecular( 78 | vec3 lightDirection, 79 | vec3 viewDirection, 80 | vec3 surfaceNormal, 81 | float roughness, 82 | float fresnel) { 83 | 84 | float VdotN = max(dot(viewDirection, surfaceNormal), 0.0); 85 | float LdotN = max(dot(lightDirection, surfaceNormal), 0.0); 86 | 87 | //Half angle vector 88 | vec3 H = normalize(lightDirection + viewDirection); 89 | 90 | //Geometric term 91 | float NdotH = max(dot(surfaceNormal, H), 0.0); 92 | float VdotH = max(dot(viewDirection, H), 0.000001); 93 | float x = 2.0 * NdotH / VdotH; 94 | float G = min(1.0, min(x * VdotN, x * LdotN)); 95 | 96 | //Distribution term 97 | float D = beckmannDistribution(NdotH, roughness); 98 | 99 | //Fresnel term 100 | float F = pow(1.0 - VdotN, fresnel); 101 | 102 | //Multiply terms and done 103 | return G * F * D / max(3.14159265 * VdotN * LdotN, 0.000001); 104 | } 105 | 106 | void main() { 107 | float time = iGlobalTime * 0.025; 108 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution - 1.0; 109 | vec3 ro = vec3(sin(time), 1.0, cos(time)); 110 | vec3 ta = vec3(0); 111 | vec3 rd = getRay(ro, ta, uv, 2.0); 112 | 113 | float t = -1.0; 114 | float mind = 0.01; 115 | float maxd = 10.0; 116 | float latest = 1.0; 117 | for (int i = 0; i < 30; i++) { 118 | if (latest < mind || t > maxd) break; 119 | t += (latest = getDistanceFromPoint(ro + rd * t)); 120 | } 121 | 122 | float tPlane = intersectPlane(ro, rd, vec3(0, 1, 0), 0.0); 123 | 124 | if (tPlane > -0.5 && tPlane < t) { 125 | vec3 pos = ro + rd * tPlane; 126 | gl_FragColor = vec4(draw_distance(getDistanceFromPoint(pos) - 0.0125, pos.xz), 1); 127 | } else 128 | if (t > maxd) { 129 | gl_FragColor = vec4(0, 0, 0, 1); 130 | } else { 131 | vec3 pos = ro + rd * t; 132 | vec3 normal = calcNormal(pos, 0.002); 133 | vec3 ldir = normalize(vec3(0, 1, 0.2)); 134 | float mag = max(0.2, dot(normal, ldir)); 135 | 136 | mag = pow(mag, 0.3545); 137 | mag *= 1.75; 138 | //mag = 0.0; 139 | 140 | gl_FragColor = vec4(mag * vec3(0.95, 0.45, 0.15), 1); 141 | gl_FragColor.rgb += cookTorranceSpecular(ldir, -rd, normal, 1.0, 3.25) * 1.5; 142 | } 143 | } 144 | 145 | vec3 draw_line(float d) { 146 | const float aa = 3.0; 147 | const float thickness = 0.0025; 148 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 149 | } 150 | 151 | float draw_solid(float d) { 152 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 153 | } 154 | 155 | vec3 draw_distance(float d, vec2 p) { 156 | float t = clamp(d * 0.85, 0.0, 1.0); 157 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 158 | 159 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 160 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 161 | float d2 = abs(1.0 - draw_line(d).x); 162 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 163 | 164 | grad -= rim * clamp(1.25 - d, 0.0, 1.0); 165 | grad -= 1.0 - clamp(1.25 - d * 0.25, 0.0, 1.0); 166 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 167 | 168 | return grad; 169 | } 170 | -------------------------------------------------------------------------------- /chapters/13-blending-again.md: -------------------------------------------------------------------------------- 1 | # Blending Shapes II 2 | 3 | Smooth minimum also extends to 3 dimensions easily. Since it's only comparing distances you don't even need to change the function. 4 | 5 | As a result, we can smoothly blend multiple shapes together with minimal overhead. This is an effect that's quite difficult to achieve using a traditional, triangle-based rendering setup but comes quite naturally when working with distance functions. 6 | 7 | Here's those smooth min functions again for reference: 8 | 9 | ### Exponential 10 | 11 | ``` 12 | float smin( float a, float b, float k ) 13 | { 14 | float res = exp( -k*a ) + exp( -k*b ); 15 | return -log( res )/k; 16 | } 17 | ``` 18 | 19 | ### Polynomial 20 | 21 | ``` 22 | float smin( float a, float b, float k ) 23 | { 24 | float h = clamp( 0.5+0.5*(b-a)/k, 0.0, 1.0 ); 25 | return mix( b, a, h ) - k*h*(1.0-h); 26 | } 27 | ``` 28 | 29 | ### Power 30 | 31 | ``` 32 | float smin( float a, float b, float k ) 33 | { 34 | a = pow( a, k ); b = pow( b, k ); 35 | return pow( (a*b)/(a+b), 1.0/k ); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /chapters/14-repeating-space.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | // 3 | // Try and recreate the solution by eyeballing it :) 4 | // 5 | float getDistanceFromPoint(vec3 point) { 6 | float period = 0.4; 7 | float radius = 0.1; 8 | return 0.0; 9 | } 10 | #pragma solution 11 | float getDistanceFromPoint(vec3 point) { 12 | float period = 0.4; 13 | float radius = 0.1; 14 | point.xz = mod(point.xz + period * 0.5, period) - period * 0.5; 15 | return length(point) - radius; 16 | } 17 | #pragma prefix 18 | uniform vec2 iResolution; 19 | uniform float iGlobalTime; 20 | 21 | float getDistanceFromPoint(vec3 point); 22 | vec3 draw_line(float d); 23 | float draw_solid(float d); 24 | vec3 draw_distance(float d, vec2 p); 25 | 26 | mat3 calcLookAtMatrix(vec3 origin, vec3 target, float roll) { 27 | vec3 rr = vec3(sin(roll), cos(roll), 0.0); 28 | vec3 ww = normalize(target - origin); 29 | vec3 uu = normalize(cross(ww, rr)); 30 | vec3 vv = normalize(cross(uu, ww)); 31 | 32 | return mat3(uu, vv, ww); 33 | } 34 | 35 | vec3 calcNormal(vec3 pos, float eps) { 36 | const vec3 v1 = vec3( 1.0,-1.0,-1.0); 37 | const vec3 v2 = vec3(-1.0,-1.0, 1.0); 38 | const vec3 v3 = vec3(-1.0, 1.0,-1.0); 39 | const vec3 v4 = vec3( 1.0, 1.0, 1.0); 40 | 41 | return normalize( v1 * getDistanceFromPoint( pos + v1*eps ) + 42 | v2 * getDistanceFromPoint( pos + v2*eps ) + 43 | v3 * getDistanceFromPoint( pos + v3*eps ) + 44 | v4 * getDistanceFromPoint( pos + v4*eps ) ); 45 | } 46 | 47 | vec3 getRay(vec3 origin, vec3 target, vec2 screenPos, float lensLength) { 48 | mat3 camMat = calcLookAtMatrix(origin, target, 0.0); 49 | return normalize(camMat * vec3(screenPos, lensLength)); 50 | } 51 | 52 | float intersectPlane(vec3 ro, vec3 rd, vec3 nor, float dist) { 53 | float denom = dot(rd, nor); 54 | float t = -(dot(ro, nor) + dist) / denom; 55 | 56 | return t; 57 | } 58 | #pragma suffix 59 | float beckmannDistribution(float x, float roughness) { 60 | float NdotH = max(x, 0.0001); 61 | float cos2Alpha = NdotH * NdotH; 62 | float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha; 63 | float roughness2 = roughness * roughness; 64 | float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha; 65 | return exp(tan2Alpha / roughness2) / denom; 66 | } 67 | 68 | float cookTorranceSpecular( 69 | vec3 lightDirection, 70 | vec3 viewDirection, 71 | vec3 surfaceNormal, 72 | float roughness, 73 | float fresnel) { 74 | 75 | float VdotN = max(dot(viewDirection, surfaceNormal), 0.0); 76 | float LdotN = max(dot(lightDirection, surfaceNormal), 0.0); 77 | 78 | //Half angle vector 79 | vec3 H = normalize(lightDirection + viewDirection); 80 | 81 | //Geometric term 82 | float NdotH = max(dot(surfaceNormal, H), 0.0); 83 | float VdotH = max(dot(viewDirection, H), 0.000001); 84 | float x = 2.0 * NdotH / VdotH; 85 | float G = min(1.0, min(x * VdotN, x * LdotN)); 86 | 87 | //Distribution term 88 | float D = beckmannDistribution(NdotH, roughness); 89 | 90 | //Fresnel term 91 | float F = pow(1.0 - VdotN, fresnel); 92 | 93 | //Multiply terms and done 94 | return G * F * D / max(3.14159265 * VdotN * LdotN, 0.000001); 95 | } 96 | 97 | void main() { 98 | float time = iGlobalTime * 0.0125; 99 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution - 1.0; 100 | vec3 ro = vec3(sin(time), 1.0, cos(time)); 101 | vec3 ta = vec3(0); 102 | vec3 rd = getRay(ro, ta, uv, 2.0); 103 | 104 | float t = -1.0; 105 | float mind = 0.01; 106 | float maxd = 10.0; 107 | float latest = 1.0; 108 | for (int i = 0; i < 30; i++) { 109 | if (latest < mind || t > maxd) break; 110 | t += (latest = getDistanceFromPoint(ro + rd * t)); 111 | } 112 | 113 | float tPlane = intersectPlane(ro, rd, vec3(0, 1, 0), 0.0); 114 | 115 | if (tPlane > -0.5 && tPlane < t) { 116 | vec3 pos = ro + rd * tPlane; 117 | gl_FragColor = vec4(draw_distance(getDistanceFromPoint(pos) - 0.0125, pos.xz), 1); 118 | } else 119 | if (t > maxd) { 120 | gl_FragColor = vec4(0, 0, 0, 1); 121 | } else { 122 | vec3 pos = ro + rd * t; 123 | vec3 normal = calcNormal(pos, 0.002); 124 | vec3 ldir = normalize(vec3(0, 1, 0.2)); 125 | float mag = max(0.2, dot(normal, ldir)); 126 | 127 | mag = pow(mag, 0.3545); 128 | mag *= 1.75; 129 | //mag = 0.0; 130 | 131 | gl_FragColor = vec4(mag * vec3(0.95, 0.45, 0.15), 1); 132 | gl_FragColor.rgb += cookTorranceSpecular(ldir, -rd, normal, 1.0, 3.25) * 1.5; 133 | } 134 | } 135 | 136 | vec3 draw_line(float d) { 137 | const float aa = 3.0; 138 | const float thickness = 0.0025; 139 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 140 | } 141 | 142 | float draw_solid(float d) { 143 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 144 | } 145 | 146 | vec3 draw_distance(float d, vec2 p) { 147 | float t = clamp(d * 0.85, 0.0, 1.0); 148 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 149 | 150 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 151 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 152 | float d2 = abs(1.0 - draw_line(d).x); 153 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 154 | 155 | grad -= rim * clamp(1.25 - d, 0.0, 1.0); 156 | grad -= 1.0 - clamp(1.25 - d * 0.25, 0.0, 1.0); 157 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 158 | 159 | return grad; 160 | } 161 | -------------------------------------------------------------------------------- /chapters/14-repeating-space.md: -------------------------------------------------------------------------------- 1 | # Repeating Space II 2 | 3 | You made it! Here's a slightly trickier one for you. 4 | -------------------------------------------------------------------------------- /chapters/15-repeating-space.glsl: -------------------------------------------------------------------------------- 1 | #pragma question 2 | float getDistanceFromPoint(vec3 point) { 3 | vec3 offset = vec3(0.75, 0, 0) * abs(sin(iGlobalTime * 0.1)); 4 | float radius = 0.1; 5 | 6 | return 0.0; 7 | } 8 | #pragma solution 9 | float getDistanceFromPoint(vec3 point) { 10 | vec3 offset = vec3(0.75, 0, 0) * abs(sin(iGlobalTime * 0.1)); 11 | float radius = 0.1; 12 | 13 | float angle = atan(point.z, point.x); 14 | float dist = length(point.xz); 15 | float count = 5.0; 16 | float pi = 3.14159265; 17 | float tau = pi * 2.0; 18 | float period = tau / count; 19 | 20 | angle = mod(angle + period * 0.5, period) - period * 0.5; 21 | point.xz = vec2( 22 | dist * cos(angle), 23 | dist * sin(angle) 24 | ); 25 | 26 | return length(point - offset) - radius; 27 | } 28 | #pragma prefix 29 | uniform vec2 iResolution; 30 | uniform float iGlobalTime; 31 | 32 | float getDistanceFromPoint(vec3 point); 33 | vec3 draw_line(float d); 34 | float draw_solid(float d); 35 | vec3 draw_distance(float d, vec2 p); 36 | 37 | mat3 calcLookAtMatrix(vec3 origin, vec3 target, float roll) { 38 | vec3 rr = vec3(sin(roll), cos(roll), 0.0); 39 | vec3 ww = normalize(target - origin); 40 | vec3 uu = normalize(cross(ww, rr)); 41 | vec3 vv = normalize(cross(uu, ww)); 42 | 43 | return mat3(uu, vv, ww); 44 | } 45 | 46 | vec3 calcNormal(vec3 pos, float eps) { 47 | const vec3 v1 = vec3( 1.0,-1.0,-1.0); 48 | const vec3 v2 = vec3(-1.0,-1.0, 1.0); 49 | const vec3 v3 = vec3(-1.0, 1.0,-1.0); 50 | const vec3 v4 = vec3( 1.0, 1.0, 1.0); 51 | 52 | return normalize( v1 * getDistanceFromPoint( pos + v1*eps ) + 53 | v2 * getDistanceFromPoint( pos + v2*eps ) + 54 | v3 * getDistanceFromPoint( pos + v3*eps ) + 55 | v4 * getDistanceFromPoint( pos + v4*eps ) ); 56 | } 57 | 58 | vec3 getRay(vec3 origin, vec3 target, vec2 screenPos, float lensLength) { 59 | mat3 camMat = calcLookAtMatrix(origin, target, 0.0); 60 | return normalize(camMat * vec3(screenPos, lensLength)); 61 | } 62 | 63 | float intersectPlane(vec3 ro, vec3 rd, vec3 nor, float dist) { 64 | float denom = dot(rd, nor); 65 | float t = -(dot(ro, nor) + dist) / denom; 66 | 67 | return t; 68 | } 69 | #pragma suffix 70 | float beckmannDistribution(float x, float roughness) { 71 | float NdotH = max(x, 0.0001); 72 | float cos2Alpha = NdotH * NdotH; 73 | float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha; 74 | float roughness2 = roughness * roughness; 75 | float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha; 76 | return exp(tan2Alpha / roughness2) / denom; 77 | } 78 | 79 | float cookTorranceSpecular( 80 | vec3 lightDirection, 81 | vec3 viewDirection, 82 | vec3 surfaceNormal, 83 | float roughness, 84 | float fresnel) { 85 | 86 | float VdotN = max(dot(viewDirection, surfaceNormal), 0.0); 87 | float LdotN = max(dot(lightDirection, surfaceNormal), 0.0); 88 | 89 | //Half angle vector 90 | vec3 H = normalize(lightDirection + viewDirection); 91 | 92 | //Geometric term 93 | float NdotH = max(dot(surfaceNormal, H), 0.0); 94 | float VdotH = max(dot(viewDirection, H), 0.000001); 95 | float x = 2.0 * NdotH / VdotH; 96 | float G = min(1.0, min(x * VdotN, x * LdotN)); 97 | 98 | //Distribution term 99 | float D = beckmannDistribution(NdotH, roughness); 100 | 101 | //Fresnel term 102 | float F = pow(1.0 - VdotN, fresnel); 103 | 104 | //Multiply terms and done 105 | return G * F * D / max(3.14159265 * VdotN * LdotN, 0.000001); 106 | } 107 | 108 | void main() { 109 | vec2 uv = 2.0 * gl_FragCoord.xy / iResolution - 1.0; 110 | vec3 ro = vec3(sin(0.0), 1.0, cos(0.0)); 111 | vec3 ta = vec3(0); 112 | vec3 rd = getRay(ro, ta, uv, 2.0); 113 | 114 | float t = -1.0; 115 | float mind = 0.01; 116 | float maxd = 10.0; 117 | float latest = 1.0; 118 | for (int i = 0; i < 30; i++) { 119 | if (latest < mind || t > maxd) break; 120 | t += (latest = getDistanceFromPoint(ro + rd * t)); 121 | } 122 | 123 | float tPlane = intersectPlane(ro, rd, vec3(0, 1, 0), 0.0); 124 | 125 | if (tPlane > -0.5 && tPlane < t) { 126 | vec3 pos = ro + rd * tPlane; 127 | gl_FragColor = vec4(draw_distance(getDistanceFromPoint(pos) - 0.0125, pos.xz), 1); 128 | } else 129 | if (t > maxd) { 130 | gl_FragColor = vec4(0, 0, 0, 1); 131 | } else { 132 | vec3 pos = ro + rd * t; 133 | vec3 normal = calcNormal(pos, 0.002); 134 | vec3 ldir = normalize(vec3(0, 1, 0.2)); 135 | float mag = max(0.2, dot(normal, ldir)); 136 | 137 | mag = pow(mag, 0.3545); 138 | mag *= 1.75; 139 | //mag = 0.0; 140 | 141 | gl_FragColor = vec4(mag * vec3(0.95, 0.45, 0.15), 1); 142 | gl_FragColor.rgb += cookTorranceSpecular(ldir, -rd, normal, 1.0, 3.25) * 1.5; 143 | } 144 | } 145 | 146 | vec3 draw_line(float d) { 147 | const float aa = 3.0; 148 | const float thickness = 0.0025; 149 | return vec3(smoothstep(0.0, aa / iResolution.y, max(0.0, abs(d) - thickness))); 150 | } 151 | 152 | float draw_solid(float d) { 153 | return smoothstep(0.0, 3.0 / iResolution.y, max(0.0, d)); 154 | } 155 | 156 | vec3 draw_distance(float d, vec2 p) { 157 | float t = clamp(d * 0.85, 0.0, 1.0); 158 | vec3 grad = mix(vec3(1, 0.8, 0.5), vec3(0.3, 0.8, 1), t); 159 | 160 | float d0 = abs(1.0 - draw_line(mod(d + 0.1, 0.2) - 0.1).x); 161 | float d1 = abs(1.0 - draw_line(mod(d + 0.025, 0.05) - 0.025).x); 162 | float d2 = abs(1.0 - draw_line(d).x); 163 | vec3 rim = vec3(max(d2 * 0.85, max(d0 * 0.25, d1 * 0.06125))); 164 | 165 | grad -= rim * clamp(1.25 - d, 0.0, 1.0); 166 | grad -= 1.0 - clamp(1.25 - d * 0.25, 0.0, 1.0); 167 | grad -= mix(vec3(0.05, 0.35, 0.35), vec3(0.0), draw_solid(d)); 168 | 169 | return grad; 170 | } 171 | -------------------------------------------------------------------------------- /chapters/15-repeating-space.md: -------------------------------------------------------------------------------- 1 | # Repeating Space III 2 | 3 | Ok, here's one last one. We're using a new trick that's not been covered in the workshop yet. Can you work out how it works? 4 | -------------------------------------------------------------------------------- /chapters/_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |{{chapterData}}35 | 36 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /display.js: -------------------------------------------------------------------------------- 1 | const debounce = require('frame-debounce') 2 | 3 | const figure = document.querySelector('#display') 4 | const canvas = figure.querySelector('canvas') 5 | const caption = figure.querySelector('figcaption') 6 | 7 | const PAGE_OFFSET = 7 * 16 8 | 9 | class Display { 10 | constructor (figure, canvas, caption) { 11 | this.figure = figure 12 | this.canvas = canvas 13 | this.caption = caption 14 | this.gl = ( 15 | this.canvas.getContext('webgl') || 16 | this.canvas.getContext('experimental-webgl') 17 | ) 18 | 19 | console.log(this.gl.getExtension('OES_standard_derivatives')) 20 | this.currDisplay = null 21 | this.displays = [] 22 | this.recalcTops = debounce(this.recalcTops.bind(this), false) 23 | this._visible = false 24 | 25 | window.addEventListener('scroll', debounce(() => this.onScroll()), false) 26 | setTimeout(() => this.onScroll()) 27 | 28 | var self = this 29 | 30 | loop() 31 | function loop () { 32 | window.requestAnimationFrame(loop) 33 | if (!self.currDisplay) return 34 | self.currDisplay.step(self.gl) 35 | } 36 | } 37 | 38 | get visible () { return this._visible } 39 | set visible (next) { 40 | if (this._visible === next) return 41 | this._visible = next 42 | if (next) { 43 | document.body.classList.remove('canvas-hidden') 44 | } else { 45 | document.body.classList.add('canvas-hidden') 46 | } 47 | } 48 | 49 | register (element, options, init, step, exit) { 50 | this.displays.push({ element, options, init, step, exit, top: null }) 51 | this.recalcTops() 52 | } 53 | 54 | recalcTops () { 55 | var displays = this.displays 56 | var scroll = window.scrollY 57 | 58 | for (var i = 0; i < displays.length; i++) { 59 | displays[i].top = displays[i].element.getBoundingClientRect().top + scroll + PAGE_OFFSET 60 | } 61 | 62 | displays.sort(function (a, b) { 63 | return b.top - a.top 64 | }) 65 | } 66 | 67 | onScroll () { 68 | var scroll = window.scrollY 69 | var height = window.innerHeight 70 | var displays = this.displays 71 | var center = scroll + height * 0.35 72 | 73 | var closestIdx = -1 74 | var closestDst = Infinity 75 | 76 | for (var i = 0; i < displays.length; i++) { 77 | var display = displays[i] 78 | var distance = display.top - scroll 79 | if (distance < 0) continue 80 | if (distance > height * 0.9) continue 81 | 82 | var closeness = Math.abs(display.top - center) 83 | if (closeness > closestDst) continue 84 | 85 | closestDst = closeness 86 | closestIdx = i 87 | } 88 | 89 | this.visible = closestIdx !== -1 90 | this.changeDisplay(this.visible ? displays[closestIdx] : null) 91 | } 92 | 93 | changeDisplay (next) { 94 | var curr = this.currDisplay 95 | if (curr === next) return 96 | if (curr) curr.exit(this.gl) 97 | if (next) next.init(this.gl) 98 | this.currDisplay = next 99 | } 100 | } 101 | 102 | module.exports = new Display(figure, canvas, caption) 103 | -------------------------------------------------------------------------------- /editor-mode-glsl.js: -------------------------------------------------------------------------------- 1 | module.exports = function(CodeMirror) { 2 | CodeMirror.defineMode("glsl", function(config, parserConfig) { 3 | var indentUnit = config.indentUnit, 4 | keywords = parserConfig.keywords || words(glslKeywords), 5 | builtins = parserConfig.builtins || words(glslBuiltins), 6 | blockKeywords = parserConfig.blockKeywords || words("case do else for if switch while struct"), 7 | atoms = parserConfig.atoms || words("null"), 8 | hooks = parserConfig.hooks || {}, 9 | multiLineStrings = parserConfig.multiLineStrings; 10 | var isOperatorChar = /[+\-*&%=<>!?|\/]/; 11 | 12 | var curPunc; 13 | 14 | function tokenBase(stream, state) { 15 | var ch = stream.next(); 16 | if (hooks[ch]) { 17 | var result = hooks[ch](stream, state); 18 | if (result !== false) return result; 19 | } 20 | if (ch == '"' || ch == "'") { 21 | state.tokenize = tokenString(ch); 22 | return state.tokenize(stream, state); 23 | } 24 | if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 25 | curPunc = ch; 26 | return "bracket"; 27 | } 28 | if (/\d/.test(ch)) { 29 | stream.eatWhile(/[\w\.]/); 30 | return "number"; 31 | } 32 | if (ch == "/") { 33 | if (stream.eat("*")) { 34 | state.tokenize = tokenComment; 35 | return tokenComment(stream, state); 36 | } 37 | if (stream.eat("/")) { 38 | stream.skipToEnd(); 39 | return "comment"; 40 | } 41 | } 42 | if (ch == "#") { 43 | stream.eatWhile(/[\S]+/); 44 | stream.eatWhile(/[\s]+/); 45 | stream.eatWhile(/[\S]+/); 46 | stream.eatWhile(/[\s]+/); 47 | return "comment"; 48 | } 49 | if (isOperatorChar.test(ch)) { 50 | stream.eatWhile(isOperatorChar); 51 | return "operator"; 52 | } 53 | stream.eatWhile(/[\w\$_]/); 54 | var cur = stream.current(); 55 | if (keywords.propertyIsEnumerable(cur)) { 56 | if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; 57 | return "keyword"; 58 | } 59 | if (builtins.propertyIsEnumerable(cur)) { 60 | return "builtin"; 61 | } 62 | if (atoms.propertyIsEnumerable(cur)) return "atom"; 63 | return "word"; 64 | } 65 | 66 | function tokenString(quote) { 67 | return function(stream, state) { 68 | var escaped = false, next, end = false; 69 | while ((next = stream.next()) != null) { 70 | if (next == quote && !escaped) {end = true; break;} 71 | escaped = !escaped && next == "\\"; 72 | } 73 | if (end || !(escaped || multiLineStrings)) 74 | state.tokenize = tokenBase; 75 | return "string"; 76 | }; 77 | } 78 | 79 | function tokenComment(stream, state) { 80 | var maybeEnd = false, ch; 81 | while (ch = stream.next()) { 82 | if (ch == "/" && maybeEnd) { 83 | state.tokenize = tokenBase; 84 | break; 85 | } 86 | maybeEnd = (ch == "*"); 87 | } 88 | return "comment"; 89 | } 90 | 91 | function Context(indented, column, type, align, prev) { 92 | this.indented = indented; 93 | this.column = column; 94 | this.type = type; 95 | this.align = align; 96 | this.prev = prev; 97 | } 98 | function pushContext(state, col, type) { 99 | return state.context = new Context(state.indented, col, type, null, state.context); 100 | } 101 | function popContext(state) { 102 | var t = state.context.type; 103 | if (t == ")" || t == "]" || t == "}") 104 | state.indented = state.context.indented; 105 | return state.context = state.context.prev; 106 | } 107 | 108 | // Interface 109 | 110 | return { 111 | startState: function(basecolumn) { 112 | return { 113 | tokenize: null, 114 | context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), 115 | indented: 0, 116 | startOfLine: true 117 | }; 118 | }, 119 | 120 | token: function(stream, state) { 121 | var ctx = state.context; 122 | if (stream.sol()) { 123 | if (ctx.align == null) ctx.align = false; 124 | state.indented = stream.indentation(); 125 | state.startOfLine = true; 126 | } 127 | if (stream.eatSpace()) return null; 128 | curPunc = null; 129 | var style = (state.tokenize || tokenBase)(stream, state); 130 | if (style == "comment" || style == "meta") return style; 131 | if (ctx.align == null) ctx.align = true; 132 | 133 | if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state); 134 | else if (curPunc == "{") pushContext(state, stream.column(), "}"); 135 | else if (curPunc == "[") pushContext(state, stream.column(), "]"); 136 | else if (curPunc == "(") pushContext(state, stream.column(), ")"); 137 | else if (curPunc == "}") { 138 | while (ctx.type == "statement") ctx = popContext(state); 139 | if (ctx.type == "}") ctx = popContext(state); 140 | while (ctx.type == "statement") ctx = popContext(state); 141 | } 142 | else if (curPunc == ctx.type) popContext(state); 143 | else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement")) 144 | pushContext(state, stream.column(), "statement"); 145 | state.startOfLine = false; 146 | return style; 147 | }, 148 | 149 | indent: function(state, textAfter) { 150 | if (state.tokenize != tokenBase && state.tokenize != null) return 0; 151 | var firstChar = textAfter && textAfter.charAt(0), ctx = state.context, closing = firstChar == ctx.type; 152 | if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit); 153 | else if (ctx.align) return ctx.column + (closing ? 0 : 1); 154 | else return ctx.indented + (closing ? 0 : indentUnit); 155 | }, 156 | 157 | electricChars: "{}" 158 | }; 159 | }); 160 | 161 | function words(str) { 162 | var obj = {}, words = str.split(" "); 163 | for (var i = 0; i < words.length; ++i) obj[words[i]] = true; 164 | return obj; 165 | } 166 | var glslKeywords = "attribute const uniform varying break continue " + 167 | "do for while if else in out inout float int void bool true false " + 168 | "lowp mediump highp precision invariant discard return mat2 mat3 " + 169 | "mat4 vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 sampler2D " + 170 | "samplerCube struct gl_FragCoord gl_FragColor"; 171 | var glslBuiltins = "radians degrees sin cos tan asin acos atan pow " + 172 | "exp log exp2 log2 sqrt inversesqrt abs sign floor ceil fract mod " + 173 | "min max clamp mix step smoothstep length distance dot cross " + 174 | "normalize faceforward reflect refract matrixCompMult lessThan " + 175 | "lessThanEqual greaterThan greaterThanEqual equal notEqual any all " + 176 | "not dFdx dFdy fwidth texture2D texture2DProj texture2DLod " + 177 | "texture2DProjLod textureCube textureCubeLod require export"; 178 | 179 | function cppHook(stream, state) { 180 | if (!state.startOfLine) return false; 181 | stream.skipToEnd(); 182 | return "meta"; 183 | } 184 | 185 | ;(function() { 186 | // C#-style strings where "" escapes a quote. 187 | function tokenAtString(stream, state) { 188 | var next; 189 | while ((next = stream.next()) != null) { 190 | if (next == '"' && !stream.eat('"')) { 191 | state.tokenize = null; 192 | break; 193 | } 194 | } 195 | return "string"; 196 | } 197 | 198 | CodeMirror.defineMIME("text/x-glsl", { 199 | name: "glsl", 200 | keywords: words(glslKeywords), 201 | builtins: words(glslBuiltins), 202 | blockKeywords: words("case do else for if switch while struct"), 203 | atoms: words("null"), 204 | hooks: {"#": cppHook} 205 | }); 206 | }()); 207 | } 208 | -------------------------------------------------------------------------------- /editor.js: -------------------------------------------------------------------------------- 1 | const errorParser = require('gl-shader-errors') 2 | const triangle = require('a-big-triangle') 3 | const tokenize = require('glsl-tokenizer') 4 | const CodeMirror = require('codemirror') 5 | const insert = require('defaultcss') 6 | const unescape = require('unescape') 7 | const Shader = require('gl-shader') 8 | const Fit = require('canvas-fit') 9 | const fs = require('fs') 10 | 11 | require('./editor-mode-glsl')(CodeMirror) 12 | insert('cmtheme', fs.readFileSync(require.resolve('codemirror/theme/xq-light.css'), 'utf8')) 13 | insert('cm-main', fs.readFileSync(require.resolve('codemirror/lib/codemirror.css'), 'utf8')) 14 | 15 | module.exports = createEditor 16 | 17 | function createEditor () { 18 | var container = document.querySelector('.editor') 19 | var data = JSON.parse(unescape(document.getElementById('chapter-data').innerHTML)) 20 | var vert = ` 21 | precision mediump float; 22 | 23 | attribute vec2 position; 24 | varying vec2 uv; 25 | 26 | void main() { 27 | uv = position * 0.5 + 0.5; 28 | gl_Position = vec4(position, 1, 1); 29 | } 30 | ` 31 | 32 | var lessonKey = 'lesson:' + data.name 33 | var cOpts = { preserveDrawingBuffer: true } 34 | var displayQuestion = document.querySelector('#canvas-us') 35 | var displaySolution = document.querySelector('#canvas-them') 36 | var canvasQuestion = displayQuestion.appendChild(document.createElement('canvas')) 37 | var canvasSolution = displaySolution.appendChild(document.createElement('canvas')) 38 | var glq = canvasQuestion.getContext('webgl', cOpts) || canvasQuestion.getContext('experimental-webgl', cOpts) 39 | var gls = canvasSolution.getContext('webgl', cOpts) || canvasSolution.getContext('experimental-webgl', cOpts) 40 | glq.getExtension('OES_standard_derivatives') 41 | gls.getExtension('OES_standard_derivatives') 42 | var shaderQuestion = Shader(glq, vert, getFrag(data.question)) 43 | var shaderSolution = Shader(gls, vert, getFrag(data.solution)) 44 | 45 | var currTime = 0 46 | var start = Date.now() 47 | var shape = [] 48 | 49 | var fitQuestion = Fit(canvasQuestion) 50 | var fitSolution = Fit(canvasSolution) 51 | var rem = 16 52 | 53 | function resize (e) { 54 | var height = window.innerHeight - 5 * rem - 2 55 | displayQuestion.parentNode.style.minWidth = Math.ceil(height / 2) + 'px' 56 | fitQuestion(e) 57 | fitSolution(e) 58 | canvasSolution.width = canvasQuestion.width 59 | canvasSolution.height = canvasQuestion.height 60 | matchOffset = 0 61 | } 62 | 63 | function loop () { 64 | currTime = (Date.now() - start) / 100 65 | window.requestAnimationFrame(loop) 66 | 67 | draw(glq, shaderQuestion) 68 | draw(gls, shaderSolution) 69 | matchPoll() 70 | } 71 | 72 | // omg loads of work to compare buffers 73 | var matchLabel = document.querySelector('.mainui .checker span') 74 | var pixelBuffer1 = new Uint8Array(4 * 512 * 512) 75 | var pixelBuffer2 = new Uint8Array(4 * 512 * 512) 76 | var failedMatch = false 77 | var passedMatch = false 78 | var matchOffset = 0 79 | var matchStepSize = 16 80 | var incr = 0 81 | var prefixLineCount = (data.prefix || '').split('\n').length + 2 82 | 83 | if (checkBanned(data.question)) failedMatch = true 84 | 85 | function matchPoll () { 86 | var width = canvasSolution.width 87 | var height = canvasSolution.height 88 | if (passedMatch) return 89 | if (failedMatch) return 90 | if ((incr++ % 5)) return 91 | if (matchOffset + matchStepSize >= height) { 92 | passedMatch = true 93 | matchLabel.innerHTML = 'Got it! Nice work :D' 94 | matchLabel.parentNode.style.background = '#69e61b' 95 | var doneMark = document.querySelector('.done-mark[data-name="' + data.name + '"]') 96 | if (doneMark) { 97 | doneMark.classList.add('is-done') 98 | doneMark.parentNode.classList.add('faded') 99 | } 100 | if (window.localStorage) { 101 | window.localStorage.setItem(lessonKey, String(true)) 102 | } 103 | 104 | return 105 | } 106 | var threshold = 3 * 0.01 * 255 107 | var total = 3 * width * matchStepSize 108 | var missed = 0 109 | glq.readPixels(0, matchOffset, width, matchStepSize, glq.RGBA, glq.UNSIGNED_BYTE, pixelBuffer1) 110 | gls.readPixels(0, matchOffset, width, matchStepSize, gls.RGBA, gls.UNSIGNED_BYTE, pixelBuffer2) 111 | for (var y = 0, i = 0; y < matchStepSize; y++) { 112 | for (var x = 0; x < width; x++, i++) { 113 | var r1 = pixelBuffer1[i], r2 = pixelBuffer2[i++] 114 | var g1 = pixelBuffer1[i], g2 = pixelBuffer2[i++] 115 | var b1 = pixelBuffer1[i], b2 = pixelBuffer2[i++] 116 | var diff = Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) 117 | if (diff > threshold) missed++ 118 | } 119 | } 120 | var error = missed / total 121 | if (error > 0.01) { 122 | failedMatch = true 123 | matchLabel.innerHTML = '' 124 | return 125 | } 126 | var completion = 100 * matchOffset / height 127 | matchLabel.innerHTML = 'Checking Answer: ' + completion.toFixed(2) + '%...' 128 | matchOffset += matchStepSize 129 | } 130 | 131 | function draw (gl, shader) { 132 | var width = gl.canvas.width 133 | var height = gl.canvas.height 134 | gl.viewport(0, 0, width, height) 135 | shape[0] = width 136 | shape[1] = height 137 | shader.bind() 138 | shader.uniforms.iResolution = shape 139 | shader.uniforms.iGlobalTime = currTime 140 | 141 | gl.clearColor(0, 0, 0, 1) 142 | gl.clear(gl.COLOR_BUFFER_BIT) 143 | triangle(gl) 144 | } 145 | 146 | var editor = new CodeMirror(container, { 147 | value: data.question, 148 | theme: 'xq-light', 149 | viewportMargin: Infinity, 150 | lineNumbers: true, 151 | gutters: [ 152 | 'shaderError', 153 | 'CodeMirror-linenumbers' 154 | ] 155 | }) 156 | 157 | var noop = function(){} 158 | editor.on('change', function () { 159 | var frag = getFrag(editor.getValue()) 160 | var warn = console.warn 161 | 162 | editor.clearGutter('shaderError') 163 | 164 | try { 165 | console.warn = noop 166 | shaderQuestion.update(vert, frag) 167 | console.warn = warn 168 | } catch (e) { 169 | var errors = errorParser(e.rawError) 170 | for (var i = 0; i < errors.length; i++) { 171 | var err = errors[i] 172 | var line = err.line - prefixLineCount 173 | var el = document.createElement('div') 174 | el.style.width = '8px' 175 | el.style.height = '8px' 176 | el.style.borderRadius = '8px' 177 | el.style.background = '#f00' 178 | el.style.marginTop = '6px' 179 | el.title = errors[i].message 180 | editor.setGutterMarker(line - 1, 'shaderError', el) 181 | } 182 | return 183 | } 184 | 185 | failedMatch = checkBanned(editor.getValue()) 186 | passedMatch = false 187 | matchOffset = 0 188 | }) 189 | 190 | function getFrag (src) { 191 | return '#extension GL_OES_standard_derivatives : enable\nprecision highp float;\n' + data.prefix + src + data.suffix 192 | } 193 | 194 | function checkBanned (frag) { 195 | if (!data.bannedTokens) return 196 | 197 | var tokens = tokenize(frag) 198 | for (var i = 0; i < tokens.length; i++) { 199 | if ( 200 | tokens[i].type === 'keyword' && 201 | tokens[i + 1] && 202 | tokens[i + 1].type === 'whitespace' 203 | ) continue 204 | 205 | if (data.bannedTokens.indexOf(tokens[i].data) !== -1) { 206 | failedMatch = true 207 | passedMatch = false 208 | matchOffset = 0 209 | return true 210 | } 211 | } 212 | } 213 | 214 | resize() 215 | window.addEventListener('resize', resize) 216 | window.requestAnimationFrame(loop) 217 | } 218 | 219 | // #extension GL_OES_standard_derivatives : enable 220 | // 221 | // vec3 red = vec3(1, 0, 0); 222 | // vec3 green = vec3(0, 1, 0); 223 | // vec3 blue = vec3(0, 0, 1); 224 | // vec3 cyan = vec3(0, 1, 1); 225 | // vec3 magenta = vec3(1, 0, 1); 226 | // vec3 yellow = vec3(1, 1, 0); 227 | // vec3 white = vec3(1, 1, 1); 228 | // 229 | // float aastep (float threshold, float value) { 230 | // float afwidth = length(vec2(dFdx(value), dFdy(value))) * 0.70710678118654757; 231 | // return smoothstep(threshold-afwidth, threshold+afwidth, value); 232 | // } 233 | // 234 | // void main() { 235 | // vec3 color = vec3(0); 236 | // 237 | // color += aastep(0.0, length(p) - 1.0) * white; 238 | // 239 | // gl_FragColor = vec4(color, 1); 240 | // } 241 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | @import 'https://fonts.googleapis.com/css?family=Roboto:500,700|Source+Code+Pro:500|Source+Serif+Pro:400,700'; 2 | 3 | html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:0 0}body{line-height:1}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}a{margin:0;padding:0;font-size:100%;vertical-align:baseline;background:0 0}ins{text-decoration:none}ins,mark{background-color:#ff9;color:#000}mark{font-style:italic;font-weight:700}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}input,select{vertical-align:middle} 4 | 5 | body { 6 | font-family: 'Source Serif Pro', serif; 7 | font-weight: lighter; 8 | width: 100%; height: 100; 9 | overflow: hidden; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | font-family: Roboto, sans-serif; 14 | } 15 | 16 | header { 17 | background: white; 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | height: 5rem; 23 | border-bottom: 1px solid black; 24 | z-index: 9999; 25 | } 26 | 27 | header h1 { 28 | font-weight: 700; 29 | font-size: 1.25rem; 30 | line-height: 5rem; 31 | margin-left: 2rem; 32 | } 33 | 34 | .nav-toggle { 35 | display: inline-block; 36 | position: relative; 37 | top: 0.05rem; 38 | margin-right: 0.5rem; 39 | height: 1rem; 40 | width: 1.55rem; 41 | background: #fff; 42 | } 43 | 44 | .nav-toggle .notch { 45 | position: absolute; 46 | left: 0; right: 0; 47 | height: 0.25rem; 48 | background: #000; 49 | transform: translate(0, 0); 50 | transition: transform 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55); 51 | } 52 | 53 | .nav-toggle .notch.bot { 54 | bottom: 0; 55 | } 56 | .nav-toggle .notch.mid { 57 | top: 50%; 58 | margin-top: -0.125rem; 59 | } 60 | .nav-enabled .nav-toggle .notch.bot { 61 | transform: translate(0, +0.125rem); 62 | } 63 | .nav-enabled .nav-toggle .notch.top { 64 | transform: translate(0, -0.125rem); 65 | } 66 | 67 | nav { 68 | position: fixed; 69 | top: 5rem; left: 0; 70 | overflow-x: hidden; 71 | overflow-y: auto; 72 | margin-top: 1px; 73 | bottom: 0; 74 | width: 20rem; 75 | background: #fff; 76 | z-index: 99999; 77 | border-right: 1px solid #000; 78 | transform: translate(-21rem, 0); 79 | transition: transform 0.25s; 80 | } 81 | 82 | .nav-enabled nav { 83 | transform: translate(0, 0); 84 | } 85 | 86 | nav ul li a { 87 | text-decoration: none; 88 | color: #000; 89 | font-weight: 500; 90 | font-family: Roboto, sans-serif; 91 | padding: 1rem 1rem; 92 | border-bottom: 1px solid #000; 93 | display: block; 94 | transition: color 0.1s, background-color 0.1s; 95 | } 96 | nav ul li a.faded { 97 | color: #aaa; 98 | } 99 | nav ul li a.faded:hover, 100 | nav ul li a:hover { 101 | font-weight: 700; 102 | background: #000; 103 | color: #fff; 104 | } 105 | 106 | nav ul li a .done-mark:before { 107 | color: #92EB34; 108 | content: ' '; 109 | display: inline-block; 110 | width: 1rem; 111 | text-align: center; 112 | text-shadow: 0 +1px #000, 113 | 0 -1px #000, 114 | +1px 0 #000, 115 | -1px 0 #000; 116 | } 117 | nav ul li a:hover .done-mark:before { 118 | font-weight: 500; 119 | color: #fff; 120 | } 121 | nav ul li a .done-mark.is-done:before { 122 | content: '✓'; 123 | } 124 | 125 | main { 126 | padding: 2rem; 127 | line-height: 1.7em; 128 | font-size: 1rem; 129 | /*text-align: justify;*/ 130 | } 131 | 132 | main > :first-child { 133 | margin-top: 0; 134 | } 135 | 136 | main h2 { 137 | font-size: 1.1rem; 138 | font-weight: 500; 139 | margin-bottom: 0.5rem; 140 | margin-top: 2rem; 141 | } 142 | 143 | main p { 144 | margin-bottom: 1rem; 145 | } 146 | 147 | figure.canvas { 148 | position: fixed; 149 | border: 1px solid #000; 150 | border-radius: 0.2rem; 151 | background: white; 152 | left: 35rem; 153 | top: 8rem; 154 | width: 400px; height: 400px; 155 | padding: 3px; 156 | transform: perspective(1300px) rotateY(0deg); 157 | transform-style: preserve-3d; 158 | transform-origin: 0% 50%; 159 | transition: transform 0.45s cubic-bezier(0.175, 0.885, 0.32, 1.275); 160 | backface-visibility: hidden; 161 | } 162 | 163 | figure.canvas canvas { 164 | backface-visibility: hidden; 165 | } 166 | 167 | figure.canvas figcaption { 168 | font-size: 0.7rem; 169 | line-height: 1.5em; 170 | margin-top: 1rem; 171 | font-style: italic; 172 | backface-visibility: hidden; 173 | } 174 | 175 | .canvas-hidden figure.canvas { 176 | transform-style: preserve-3d; 177 | transform-origin: 0% 50%; 178 | transform: perspective(1300px) rotateY(90deg); 179 | } 180 | 181 | .CodeMirror, pre, code { 182 | font-family: 'Source Code Pro', monospace; 183 | font-size: 0.85rem; 184 | } 185 | 186 | .CodeMirror { 187 | height: 100%; 188 | width: 100%; 189 | margin-bottom: 1.5rem; 190 | line-height: 1.6em; 191 | font-size: 0.75rem; 192 | font-weight: 500; 193 | background-image: none!important; 194 | } 195 | 196 | .CodeMirror-gutters { 197 | background: transparent; 198 | border: none; 199 | padding-right: 0.5rem; 200 | } 201 | 202 | .CodeMirror-linenumber { 203 | color: #d5d5d5; 204 | } 205 | 206 | .mainui { 207 | position: absolute; 208 | display: flex; 209 | top: 5rem; 210 | bottom: 0; 211 | overflow: hidden; 212 | left: 0; right: 0; 213 | box-sizing: border-box; 214 | align-items: stretch; 215 | justify-content: space-between; 216 | } 217 | 218 | .mainui .text { 219 | border-left: 1px solid #000; 220 | padding-left: 2rem; 221 | width: 25rem; 222 | overflow-y: auto; 223 | } 224 | 225 | .mainui .canvas { 226 | position: relative; 227 | display: flex; 228 | flex-direction: column; 229 | border-right: 1px solid #000; 230 | min-width: 20vw; 231 | } 232 | 233 | .mainui .label { 234 | position: absolute; 235 | bottom: 0.5rem; 236 | left: 0.5rem; 237 | background: rgba(0,0,0,0.75); 238 | color: #fff; 239 | z-index: 5; 240 | font-size: 0.9rem; 241 | font-family: Roboto, sans-serif; 242 | padding: 0.5rem 0.65rem; 243 | border-radius: 0.25rem; 244 | } 245 | 246 | .mainui #canvas-us, 247 | .mainui #canvas-them { 248 | flex: 1; 249 | position: relative; 250 | background: #888; 251 | overflow: hidden; 252 | } 253 | 254 | .mainui #canvas-us { 255 | border-bottom: 1px solid #000; 256 | } 257 | 258 | .mainui .editor { 259 | flex: 1; 260 | position: relative; 261 | padding: 2rem; 262 | padding-bottom: 4rem; 263 | } 264 | 265 | .mainui .checker { 266 | position: absolute; 267 | bottom: 0; 268 | left: 0; right: 0; 269 | background: #fff; 270 | transition: background 0.5s; 271 | border-top: 1px solid #000; 272 | height: 2rem; 273 | display: flex; 274 | text-align: center; 275 | flex-direction: row; 276 | align-items: center; 277 | justify-content: center; 278 | font-size: 0.85rem; 279 | font-family: Roboto, sans-serif; 280 | } 281 | 282 | .mainui .editor-container { 283 | position: relative; 284 | } 285 | 286 | main iframe { 287 | max-width: 100%; 288 | margin: 1rem 0; 289 | } 290 | 291 | main ul { 292 | margin: 1rem; 293 | margin-left: 2rem; 294 | } 295 | 296 | main a { 297 | color: #0080FF; 298 | border-bottom: 1px solid #0080FF; 299 | text-decoration: none; 300 | } 301 | 302 | main h3 { 303 | text-transform: uppercase; 304 | font-size: 0.8rem; 305 | margin-top: 1.25rem; 306 | margin-bottom: 0.25rem; 307 | font-weight: 500; 308 | } 309 | 310 | main pre { 311 | line-height: 1.5em; 312 | } 313 | main pre code { 314 | font-size: 0.75rem; 315 | font-weight: 300; 316 | color: #555; 317 | } 318 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |