• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 13488918148

24 Feb 2025 01:18AM UTC coverage: 89.318% (-0.7%) from 90.052%
13488918148

push

github

web-flow
feat: Implement Uniform Buffers Object Support (#3360)

This PR implements UBO support in Excalibur

```typescript
var material = game.graphicsContext.createMaterial({
  name: 'light-material',
  fragmentSource: `#version 300 es
    precision mediump float;

    struct Light {
     vec2 pos;
     float radius;
     vec4 color;
    };

    layout(std140) uniform Lighting {
        Light lights[2];
    };

    in vec2 v_uv;
    out vec4 color;

    void main() {
        float distanceToLights = 1.0;
        vec4 finalColor = vec4(0.0, 0.0, 0.0, 1.0);
        for (int i = 0; i < 2; i++) {
            float dist = length(lights[i].pos - v_uv);
            dist = smoothstep(lights[i].radius-.2, lights[i].radius+.2, dist);
            finalColor += lights[i].color * (1.0 - dist);
        }

        color = finalColor;
        // premultiply alpha
        color.rgb = color.rgb * color.a;
    }`,
  uniforms: {
    // prettier-ignore
    Lighting: new Float32Array([
      0.5, 0.5, 0.1, 0.1, // light 1 pos
      0, 1, 0, 1, // light 1 color
      1, 1, 0.1, 0.1, // light2 pos
      0, 0, 1, 1 // light 2 color
    ])
  }
}) as ex.Material;

actor.graphics.material = material;
ex.coroutine(
  game,
  function* () {
    let time = 0;
    while (true) {
      const elapsed = yield;
      time += elapsed / 1000;
      const x1 = Math.cos(time);
      const y1 = Math.sin(time);
      const x2 = Math.cos(-time);
      const y2 = Math.sin(-time);

      // prettier-ignore
      material.uniforms.Lighting = new Float32Array([
        0.2 * x1 + 0.2, 0.2 * y1 + 0.2, 0.1, 0,
        0, 1, 0, 1,
        0.5 * x2 + 0.5, 0.5 * y2 + 0.5, 0.1, 0,
        0, 0, 1, 1
      ]);    }
  }.bind(this)
);

game.start(new ex.Loader([tex]));
```

Additionally there are some DX enhancements to working with uniforms
- You can now set uniform values directly on materials/shader instances and will be upload on next `.use()`
   `material.uniforms.Lighting = ...`

6332 of 8231 branches covered (76.93%)

79 of 209 new or added lines in 9 files covered. (37.8%)

5 existing lines in 1 file now uncovered.

13839 of 15494 relevant lines covered (89.32%)

25321.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.68
/src/engine/Graphics/Context/material.ts
1
import { Color } from '../../Color';
2
import { ExcaliburGraphicsContext } from './ExcaliburGraphicsContext';
3
import { ExcaliburGraphicsContextWebGL } from './ExcaliburGraphicsContextWebGL';
4
import { Shader, UniformDictionary } from './shader';
5
import { Logger } from '../../Util/Log';
6
import { ImageSource } from '../ImageSource';
7
import { ImageFiltering } from '../Filtering';
8

9
export interface MaterialOptions {
10
  /**
11
   * Name the material for debugging
12
   */
13
  name?: string;
14

15
  /**
16
   * Excalibur graphics context to create the material (only WebGL is supported at the moment)
17
   */
18
  graphicsContext?: ExcaliburGraphicsContext;
19

20
  /**
21
   * Optionally specify a vertex shader
22
   *
23
   * If none supplied the default will be used
24
   *
25
   * ```
26
   *  #version 300 es
27
   *  // vertex position in local space
28
   *  in vec2 a_position;
29
   *  in vec2 a_uv;
30
   *  out vec2 v_uv;
31
   *  // orthographic projection matrix
32
   *  uniform mat4 u_matrix;
33
   *  // world space transform matrix
34
   *  uniform mat4 u_transform;
35
   *  void main() {
36
   *    // Set the vertex position using the ortho & transform matrix
37
   *    gl_Position = u_matrix * u_transform * vec4(a_position, 0.0, 1.0);
38
   *    // Pass through the UV coord to the fragment shader
39
   *    v_uv = a_uv;
40
   *  }
41
   * ```
42
   */
43
  vertexSource?: string;
44

45
  /**
46
   * Add custom fragment shader
47
   *
48
   * *Note: Excalibur image alpha's are pre-multiplied
49
   *
50
   * Pre-built varyings:
51
   *
52
   * * `in vec2 v_uv` - UV coordinate
53
   * * `in vec2 v_screenuv` - UV coordinate
54
   *
55
   * Pre-built uniforms:
56
   *
57
   * * `uniform sampler2D u_graphic` - The current graphic displayed by the GraphicsComponent
58
   * * `uniform vec2 u_resolution` - The current resolution of the screen
59
   * * `uniform vec2 u_size;` - The current size of the graphic
60
   * * `uniform vec4 u_color` - The current color of the material
61
   * * `uniform float u_opacity` - The current opacity of the graphics context
62
   *
63
   */
64
  fragmentSource: string;
65

66
  /**
67
   * Add custom color, by default ex.Color.Transparent
68
   */
69
  color?: Color;
70

71
  /**
72
   * Add additional images to the material, you are limited by the GPU's maximum texture slots
73
   *
74
   * Specify a dictionary of uniform sampler names to ImageSource
75
   */
76
  images?: Record<string, ImageSource>;
77

78
  /**
79
   * Optionally set starting uniforms on a shader
80
   */
81
  uniforms?: UniformDictionary;
82
}
83

84
const defaultVertexSource = `#version 300 es
1✔
85
in vec2 a_position;
86

87
in vec2 a_uv;
88
out vec2 v_uv;
89

90
in vec2 a_screenuv;
91
out vec2 v_screenuv;
92

93
uniform mat4 u_matrix;
94
uniform mat4 u_transform;
95

96
void main() {
97
  // Set the vertex position using the ortho & transform matrix
98
  gl_Position = u_matrix * u_transform * vec4(a_position, 0.0, 1.0);
99

100
  // Pass through the UV coord to the fragment shader
101
  v_uv = a_uv;
102
  v_screenuv = a_screenuv;
103
}
104
`;
105

106
export interface MaterialImageOptions {
107
  filtering?: ImageFiltering;
108
}
109

110
export class Material {
111
  private _logger = Logger.getInstance();
10✔
112
  private _name: string;
113
  private _shader!: Shader;
114
  private _color: Color = Color.Transparent;
10✔
115
  private _initialized = false;
10✔
116
  private _fragmentSource: string;
117
  private _vertexSource: string;
118

119
  private _images: Record<string, ImageSource> = {};
10✔
120
  private _uniforms: UniformDictionary = {};
10✔
121

122
  constructor(options: MaterialOptions) {
123
    const { color, name, vertexSource, fragmentSource, graphicsContext, images, uniforms } = options;
10✔
124

125
    this._name = name ?? 'anonymous material';
10!
126
    this._vertexSource = vertexSource ?? defaultVertexSource;
10!
127
    this._fragmentSource = fragmentSource;
10✔
128
    this._color = color ?? this._color;
10✔
129
    this._uniforms = uniforms ?? this._uniforms;
10!
130
    this._images = images ?? this._images;
10✔
131

132
    if (!graphicsContext) {
10!
133
      throw Error(`Material ${name} must be provided an excalibur webgl graphics context`);
×
134
    }
135

136
    if (graphicsContext instanceof ExcaliburGraphicsContextWebGL) {
10!
137
      this._initialize(graphicsContext);
10✔
138
    } else {
139
      this._logger.warn(`Material ${name} was created in 2D Canvas mode, currently only WebGL is supported`);
×
140
    }
141
  }
142

143
  private _initialize(graphicsContextWebGL: ExcaliburGraphicsContextWebGL) {
144
    if (this._initialized) {
10!
145
      return;
×
146
    }
147

148
    this._shader = graphicsContextWebGL.createShader({
10✔
149
      name: this._name,
150
      vertexSource: this._vertexSource,
151
      fragmentSource: this._fragmentSource,
152
      uniforms: this._uniforms,
153
      images: this._images,
154
      // max texture slots
155
      // - 2 for the graphic texture and screen texture
156
      // - 1 if just graphic
157
      startingTextureSlot: this.isUsingScreenTexture ? 2 : 1
10✔
158
    });
159
    this._initialized = true;
10✔
160
  }
161

162
  public get uniforms(): UniformDictionary {
NEW
163
    return this._shader.uniforms;
×
164
  }
165

166
  public get images(): Record<string, ImageSource> {
NEW
167
    return this._shader.images;
×
168
  }
169

170
  get color(): Color {
171
    return this._color;
×
172
  }
173

174
  set color(c: Color) {
175
    this._color = c;
×
176
  }
177

178
  get name() {
179
    return this._name;
1✔
180
  }
181

182
  get isUsingScreenTexture() {
183
    return this._fragmentSource.includes('u_screen_texture');
24✔
184
  }
185

186
  update(callback: (shader: Shader) => any) {
187
    if (this._shader) {
1!
188
      this._shader.use();
1✔
189
      callback(this._shader);
1✔
190
    }
191
  }
192

193
  getShader(): Shader | null {
194
    return this._shader;
7✔
195
  }
196

197
  addImageSource(samplerName: string, image: ImageSource) {
NEW
198
    this._shader.addImageSource(samplerName, image);
×
199
  }
200

201
  removeImageSource(samplerName: string) {
NEW
202
    this._shader.removeImageSource(samplerName);
×
203
  }
204

205
  use() {
206
    if (this._initialized) {
8!
207
      // bind the shader
208
      this._shader.use();
8✔
209
      // Apply standard uniforms
210
      this._shader.trySetUniformFloatColor('u_color', this._color);
8✔
211
    } else {
212
      throw Error(`Material ${this.name} not yet initialized, use the ExcaliburGraphicsContext.createMaterial() to work around this.`);
×
213
    }
214
  }
215
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc