From 0648ea99e7f28bcc713148d0317ada680e92e5b8 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Thu, 18 Jan 2024 09:12:21 -0600 Subject: [PATCH] docs: Add material docs --- site/docs/04-graphics/4.2-material.mdx | 312 ++++++++++++++++++ .../04-graphics/examples/outline-material.ts | 70 ++++ .../04-graphics/examples/screen-material.ts | 146 ++++++++ .../04-graphics/examples/simple-material.ts | 31 ++ site/docusaurus.config.ts | 2 +- 5 files changed, 560 insertions(+), 1 deletion(-) create mode 100644 site/docs/04-graphics/4.2-material.mdx create mode 100644 site/docs/04-graphics/examples/outline-material.ts create mode 100644 site/docs/04-graphics/examples/screen-material.ts create mode 100644 site/docs/04-graphics/examples/simple-material.ts diff --git a/site/docs/04-graphics/4.2-material.mdx b/site/docs/04-graphics/4.2-material.mdx new file mode 100644 index 000000000..4d1510da6 --- /dev/null +++ b/site/docs/04-graphics/4.2-material.mdx @@ -0,0 +1,312 @@ +--- +title: Materials +slug: /materials +section: Graphics +--- + +import SimpleMaterial from '!!raw-loader!./examples/simple-material.ts'; +import OutlineMaterial from '!!raw-loader!./examples/outline-material.ts'; +import ScreenMaterial from '!!raw-loader!./examples/screen-material.ts'; + +```twoslash include ex +/// +declare const game: ex.Engine; +``` + +Sometimes you need even more control about how your graphics render in your game! For example you might want a custom blend, a reflection effect, or a warping effect. With this api you can provide custom shaders to graphics. + +:::warning + +Materials are only supported in WebGL mode, this is the default mode for Excalibur but if you switch or fallback to 2D Canvas materials will not have an effect. + +::: + +## Creating Materials + +We recommend using the [[ExcaliburGraphicsContext.createMaterial]] method to create materials. + +```ts twoslash +// @include: ex +// ---cut--- + +const actor = new ex.Actor({ + pos: ex.vec(200, 200), + width: 100, + height: 100, + color: ex.Color.Blue // Default graphic will be modified by the material +}); + +// Simple material that colors every pixel red +const material = game.graphicsContext.createMaterial({ + name: 'custom-material', + fragmentSource: `#version 300 es + precision mediump float; + out vec4 color; + void main() { + color = vec4(1.0, 0.0, 0.0, 1.0); + }` +}); + +// Material applied to the actor's graphics +actor.graphics.material = material; + +``` + +:::note + +Excalibur only supports GLSL ES 300 shaders `#version 300 es`, this version declaration must be the very first thing. No spaces or newlines before it or WebGL will not compile it. + +::: + +Once a [[Material|material]] is created it can be added/removed from the [[Actor|actor's]] [[GraphicsComponent|graphics component]] by accessing the [[GraphicsComponent.material]] property. + +## Passing parameters to Materials + +To pass data to [[Material|materials]] we use a concept called "uniforms", you can think of these as global variables that are the same for each pixel during a draw. + +To update a materials uniform we recommend using [[Material.update]], this will automatically set up the shader context for you so it is ready for you to use. You get passed the excalibur [[Shader]] instance and you can set uniforms from there! There are a number of `trySet*` prefixed methods we recommend you use, these methods will attempt to set a uniform if it exists. + + +```ts twoslash +// @include: ex +// ---cut--- + +const actor = new ex.Actor({ + pos: ex.vec(200, 200), + width: 100, + height: 100, + color: ex.Color.Blue // Default graphic will be modified by the material +}); +const material = actor.graphics.material = game.graphicsContext.createMaterial({ + name: 'custom-material', + fragmentSource: `#version 300 es + precision mediump float; + + uniform vec2 iMouse; + + out vec4 color; + void main() { + // Change the color based on mouse position + vec2 mouseColor = iMouse / 800.0; + color = vec4(mouseColor, 0.0, 1.0); + }` +}); + +game.input.pointers.primary.on('move', evt => { + actor.pos = evt.worldPos; + material.update(shader => { + shader.trySetUniformFloatVector('iMouse', evt.worldPos); + }); +}); + +``` + + + + +## Material Built In Uniforms + +Materials come with a lot of built in uniforms you can use! + +* `float u_time_ms` - This is `performance.now()` a ms timestamp that starts from initial navigation to the page + +* `float u_opacity` - Current opacity of your graphic, [[Graphic.opacity]] + +* `vec2 u_resolution` - Current screen resolution + +* `vec2 u_graphic_resolution` - Current graphic's resolution + +* `vec2 u_size` - Current destination draw size + +* `mat4 u_matrix` - Excalibur's current orthographic projection matrix + +* `mat4 u_transform` - Excalibur's current world transform + +* `sampler2D u_graphic` - Current graphic's texture + +* `sampler2D u_screen_texture` - Current excalibur screen texture just before rendering the material + +## Material Built In attributes + +* `v_uv` - Current UV coordinate of the graphic +* `v_screenuv` - Current Screen UV coordinate behind the graphic + +## Outline Material Example + +One common use case for materials is to apply an effect to your [[Actor|actors]], a not uncommon ask is to create an outline. This can be useful for selections, attacks, etc. + +Here is the shader code for a rainbow outline. Change `outlineColorHSL` to a constant if you don't want a rainbow effect 🌈 + +The theory behind this shader is we sample up, down, left, and right of the current pixel in the graphic and color it. + +```shader +#version 300 es +precision mediump float; + +uniform float u_time_ms; +uniform sampler2D u_graphic; + +in vec2 v_uv; +in vec2 v_screenuv; +out vec4 fragColor; + +vec3 hsv2rgb(vec3 c){ + vec4 K=vec4(1.,2./3.,1./3.,3.); + return c.z*mix(K.xxx,clamp(abs(fract(c.x+K.xyz)*6.-K.w)-K.x, 0., 1.),c.y); +} + +void main() { + const float TAU = 6.28318530; + const float steps = 4.0; // up/down/left/right pixels + float radius = 2.0; + float time_sec = u_time_ms / 1000.; + + vec3 outlineColorHSL = vec3(sin(time_sec/2.0) * 1., 1., 1.); + vec2 aspect = 1.0 / vec2(textureSize(u_graphic, 0)); + + for (float i = 0.0; i < TAU; i += TAU / steps) { + // Sample image in a circular pattern + vec2 offset = vec2(sin(i), cos(i)) * aspect * radius; + vec4 col = texture(u_graphic, v_uv + offset); + + // Mix outline with background + float alpha = smoothstep(0.5, 0.7, col.a); + fragColor = mix(fragColor, vec4(hsv2rgb(outlineColorHSL), 1.0), alpha); // apply outline + } + + // Overlay original texture + vec4 mat = texture(u_graphic, v_uv); + float factor = smoothstep(0.5, 0.7, mat.a); + fragColor = mix(fragColor, mat, factor); +} +``` + +Here is the code in action! + + + +## Using the Screen Texture Example + +The screen texture gives you access to the texture right before the material is drawn, this allows you to do some cool compositing effects like creating a reflected water material! + +This shader is adapted from this tutorial in Godot https://www.youtube.com/watch?v=32jdNLTJ3zY + +The theory is sample the screen texture with a reflected coordinate, apply noise distortion to make the wobble, and some sin/cos sdf magic to create the crest at the top. + +```shader +#version 300 es +precision mediump float; + +#define NUM_NOISE_OCTAVES 20 + +// Precision-adjusted variations of https://www.shadertoy.com/view/4djSRW +float hash(float p) { p = fract(p * 0.011); p *= p + 7.5; p *= p + p; return fract(p); } +float hash(vec2 p) {vec3 p3 = fract(vec3(p.xyx) * 0.13); p3 += dot(p3, p3.yzx + 3.333); return fract((p3.x + p3.y) * p3.z); } + +float noise(float x) { + float i = floor(x); + float f = fract(x); + float u = f * f * (3.0 - 2.0 * f); + return mix(hash(i), hash(i + 1.0), u); +} + + +float noise(vec2 x) { + vec2 i = floor(x); + vec2 f = fract(x); + + // Four corners in 2D of a tile + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + // Simple 2D lerp using smoothstep envelope between the values. + // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), + // mix(c, d, smoothstep(0.0, 1.0, f.x)), + // smoothstep(0.0, 1.0, f.y))); + + // Same code, with the clamps in smoothstep and common subexpressions + // optimized away. + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +float fbm(float x) { + float v = 0.0; + float a = 0.5; + float shift = float(100); + for (int i = 0; i < NUM_NOISE_OCTAVES; ++i) { + v += a * noise(x); + x = x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + +float fbm(vec2 x) { + float v = 0.0; + float a = 0.5; + vec2 shift = vec2(100); + // Rotate to reduce axial bias + mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.50)); + for (int i = 0; i < NUM_NOISE_OCTAVES; ++i) { + v += a * noise(x); + x = rot * x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + + +uniform float u_time_ms; +uniform vec4 u_color; +uniform sampler2D u_graphic; +uniform sampler2D u_screen_texture; + +uniform vec2 u_resolution; // screen resolution +uniform vec2 u_graphic_resolution; // graphic resolution + +in vec2 v_uv; +in vec2 v_screenuv; +out vec4 fragColor; +void main() { + float time_sec = u_time_ms / 1000.; + float wave_amplitude = .525; + float wave_speed = 1.8; + float wave_period = .175; + vec2 scale = vec2(2.5, 8.5); + + float waves = v_uv.y * scale.y + + sin(v_uv.x * scale.x / wave_period - time_sec * wave_speed) * + cos(0.2 * v_uv.x * scale.x /wave_period + time_sec * wave_speed) * + wave_amplitude - wave_amplitude; + + + float distortion = noise(v_uv*scale*vec2(2.1, 1.05) + time_sec * 0.12) * .25 - .125; + + vec2 reflected_screenuv = vec2(v_screenuv.x - distortion, v_screenuv.y); + vec4 screen_color = texture(u_screen_texture, reflected_screenuv); + + vec4 wave_crest_color = vec4(1); + float wave_crest = clamp(smoothstep(0.1, 0.14, waves) - smoothstep(0.018, 0.99, waves), 0., 1.); + + fragColor.a = smoothstep(0.1, 0.12, waves); + vec3 mixColor = (u_color.rgb * u_color.a); // pre-multiplied alpha + + fragColor.rgb = mix(screen_color.rgb, mixColor, u_color.a)*fragColor.a + (wave_crest_color.rgb * wave_crest); +} +``` + + \ No newline at end of file diff --git a/site/docs/04-graphics/examples/outline-material.ts b/site/docs/04-graphics/examples/outline-material.ts new file mode 100644 index 000000000..ba79dbd77 --- /dev/null +++ b/site/docs/04-graphics/examples/outline-material.ts @@ -0,0 +1,70 @@ +const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png', false, ex.ImageFiltering.Pixel); +const loader = new ex.Loader([sword]); + +const outline = `#version 300 es +precision mediump float; + +uniform float u_time_ms; +uniform sampler2D u_graphic; + +in vec2 v_uv; +in vec2 v_screenuv; +out vec4 fragColor; + +vec3 hsv2rgb(vec3 c){ + vec4 K=vec4(1.,2./3.,1./3.,3.); + return c.z*mix(K.xxx,clamp(abs(fract(c.x+K.xyz)*6.-K.w)-K.x, 0., 1.),c.y); +} + +void main() { + const float TAU = 6.28318530; + const float steps = 4.0; // up/down/left/right pixels + float radius = 2.0; + float time_sec = u_time_ms / 1000.; + + vec3 outlineColorHSL = vec3(sin(time_sec/2.0) * 1., 1., 1.); + vec2 aspect = 1.0 / vec2(textureSize(u_graphic, 0)); + + for (float i = 0.0; i < TAU; i += TAU / steps) { + // Sample image in a circular pattern + vec2 offset = vec2(sin(i), cos(i)) * aspect * radius; + vec4 col = texture(u_graphic, v_uv + offset); + + // Mix outline with background + float alpha = smoothstep(0.5, 0.7, col.a); + fragColor = mix(fragColor, vec4(hsv2rgb(outlineColorHSL), 1.0), alpha); // apply outline + } + + // Overlay original texture + vec4 mat = texture(u_graphic, v_uv); + float factor = smoothstep(0.5, 0.7, mat.a); + fragColor = mix(fragColor, mat, factor); +} +` + +const outlineMaterial = game.graphicsContext.createMaterial({ + name: 'outline', + fragmentSource: outline +}); + + +const actor = new ex.Actor({ x: 100, y: 100, width: 50, height: 50 }); +actor.onInitialize = () => { + var sprite = new ex.Sprite({ + image: sword, + destSize: { + width: 300, + height: 300 + } + }); + actor.graphics.add(sprite); +}; +actor.graphics.material = outlineMaterial; + + +game.add(actor); +game.input.pointers.primary.on('move', evt => { + actor.pos = evt.worldPos; +}); + +game.start(loader); \ No newline at end of file diff --git a/site/docs/04-graphics/examples/screen-material.ts b/site/docs/04-graphics/examples/screen-material.ts new file mode 100644 index 000000000..b7c876c5a --- /dev/null +++ b/site/docs/04-graphics/examples/screen-material.ts @@ -0,0 +1,146 @@ +const sword = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png', false, ex.ImageFiltering.Pixel); +const loader = new ex.Loader([sword]); + +const waterFrag = `#version 300 es +precision mediump float; + +#define NUM_NOISE_OCTAVES 20 + +// Precision-adjusted variations of https://www.shadertoy.com/view/4djSRW +float hash(float p) { p = fract(p * 0.011); p *= p + 7.5; p *= p + p; return fract(p); } +float hash(vec2 p) {vec3 p3 = fract(vec3(p.xyx) * 0.13); p3 += dot(p3, p3.yzx + 3.333); return fract((p3.x + p3.y) * p3.z); } + +float noise(float x) { + float i = floor(x); + float f = fract(x); + float u = f * f * (3.0 - 2.0 * f); + return mix(hash(i), hash(i + 1.0), u); +} + + +float noise(vec2 x) { + vec2 i = floor(x); + vec2 f = fract(x); + + // Four corners in 2D of a tile + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + // Simple 2D lerp using smoothstep envelope between the values. + // return vec3(mix(mix(a, b, smoothstep(0.0, 1.0, f.x)), + // mix(c, d, smoothstep(0.0, 1.0, f.x)), + // smoothstep(0.0, 1.0, f.y))); + + // Same code, with the clamps in smoothstep and common subexpressions + // optimized away. + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +float fbm(float x) { + float v = 0.0; + float a = 0.5; + float shift = float(100); + for (int i = 0; i < NUM_NOISE_OCTAVES; ++i) { + v += a * noise(x); + x = x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + +float fbm(vec2 x) { + float v = 0.0; + float a = 0.5; + vec2 shift = vec2(100); + // Rotate to reduce axial bias + mat2 rot = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.50)); + for (int i = 0; i < NUM_NOISE_OCTAVES; ++i) { + v += a * noise(x); + x = rot * x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + + +uniform float u_time_ms; +uniform vec4 u_color; +uniform sampler2D u_graphic; +uniform sampler2D u_screen_texture; + +uniform vec2 u_resolution; // screen resolution +uniform vec2 u_graphic_resolution; // graphic resolution + +in vec2 v_uv; +in vec2 v_screenuv; +out vec4 fragColor; +void main() { + float time_sec = u_time_ms / 1000.; + float wave_amplitude = .525; + float wave_speed = 1.8; + float wave_period = .175; + vec2 scale = vec2(2.5, 8.5); + + float waves = v_uv.y * scale.y + + sin(v_uv.x * scale.x / wave_period - time_sec * wave_speed) * + cos(0.2 * v_uv.x * scale.x /wave_period + time_sec * wave_speed) * + wave_amplitude - wave_amplitude; + + + float distortion = noise(v_uv*scale*vec2(2.1, 1.05) + time_sec * 0.12) * .25 - .125; + + vec2 reflected_screenuv = vec2(v_screenuv.x - distortion, v_screenuv.y); + vec4 screen_color = texture(u_screen_texture, reflected_screenuv); + + vec4 wave_crest_color = vec4(1); + float wave_crest = clamp(smoothstep(0.1, 0.14, waves) - smoothstep(0.018, 0.99, waves), 0., 1.); + + fragColor.a = smoothstep(0.1, 0.12, waves); + vec3 mixColor = (u_color.rgb * u_color.a); // pre-multiplied alpha + + fragColor.rgb = mix(screen_color.rgb, mixColor, u_color.a)*fragColor.a + (wave_crest_color.rgb * wave_crest); +}`; + +const waterMaterial = game.graphicsContext.createMaterial({ + name: 'water', + fragmentSource: waterFrag, + color: ex.Color.fromRGB(55, 0, 200, .6) +}); + + +const actor = new ex.Actor({ x: 100, y: 100, width: 50, height: 50 }); +actor.onInitialize = () => { + var sprite = new ex.Sprite({ + image: sword, + destSize: { + width: 300, + height: 300 + } + }); + actor.graphics.add(sprite); +}; +game.add(actor); +game.input.pointers.primary.on('move', evt => { + actor.pos = evt.worldPos; +}); + + +const reflection = new ex.Actor({ + x: 0, + y: game.screen.resolution.height/2, + anchor: ex.vec(0, 0), + width: 512, + height: game.screen.resolution.height/2, + coordPlane: ex.CoordPlane.Screen, + color: ex.Color.Red // < - this gets overridden by the material +}); +reflection.graphics.material = waterMaterial; +reflection.z = 99; +game.add(reflection); + +game.start(loader); \ No newline at end of file diff --git a/site/docs/04-graphics/examples/simple-material.ts b/site/docs/04-graphics/examples/simple-material.ts new file mode 100644 index 000000000..e4015483e --- /dev/null +++ b/site/docs/04-graphics/examples/simple-material.ts @@ -0,0 +1,31 @@ +const actor = new ex.Actor({ + pos: ex.vec(200, 200), + width: 100, + height: 100, + color: ex.Color.Blue // Default graphic will be modified by the material +}); +const material = actor.graphics.material = game.graphicsContext.createMaterial({ + name: 'custom-material', + fragmentSource: `#version 300 es + precision mediump float; + + uniform vec2 iMouse; + + out vec4 color; + void main() { + // Change the color based on mouse position + vec2 mouseColor = iMouse / 800.0; + color = vec4(mouseColor, 0.0, 1.0); + }` +}); + +game.input.pointers.primary.on('move', evt => { + actor.pos = evt.worldPos; + material.update(shader => { + shader.trySetUniformFloatVector('iMouse', evt.worldPos); + }); +}); + + +game.add(actor); +game.start(); \ No newline at end of file diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts index 8ff4bbc2f..c5f6d57c7 100644 --- a/site/docusaurus.config.ts +++ b/site/docusaurus.config.ts @@ -286,7 +286,7 @@ const config: Config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, - additionalLanguages: ['bash', 'diff', 'json'] + additionalLanguages: ['bash', 'diff', 'json', 'glsl'] }, liveCodeBlock: { /**