-
-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
560 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
/// <reference path="../src/engine/excalibur.d.ts" /> | ||
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); | ||
}); | ||
}); | ||
|
||
``` | ||
|
||
<GameCodeBlock live | ||
title="Simple Material" | ||
code={SimpleMaterial} | ||
/> | ||
|
||
|
||
## 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! | ||
|
||
<GameCodeBlock live | ||
title="Outline Material" | ||
code={OutlineMaterial} | ||
/> | ||
|
||
## 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); | ||
} | ||
``` | ||
|
||
<GameCodeBlock live | ||
title="Reflection Material" | ||
code={ScreenMaterial} | ||
/> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Oops, something went wrong.