Skip to content

Commit

Permalink
docs: Add material docs
Browse files Browse the repository at this point in the history
  • Loading branch information
eonarheim committed Jan 18, 2024
1 parent 2a1b167 commit 0648ea9
Show file tree
Hide file tree
Showing 5 changed files with 560 additions and 1 deletion.
312 changes: 312 additions & 0 deletions site/docs/04-graphics/4.2-material.mdx
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}
/>
70 changes: 70 additions & 0 deletions site/docs/04-graphics/examples/outline-material.ts
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);
Loading

0 comments on commit 0648ea9

Please sign in to comment.