micro-gl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +556 -0
  2. package/package.json +37 -0
  3. package/src/2d/cameras/Camera2d.js +46 -0
  4. package/src/2d/constants.js +3 -0
  5. package/src/2d/controls/DragControls2d.js +174 -0
  6. package/src/2d/controls/PanZoomControls.js +64 -0
  7. package/src/2d/core/GpuResources2d.js +163 -0
  8. package/src/2d/core/InstancedShape2d.js +74 -0
  9. package/src/2d/core/Object2d.js +107 -0
  10. package/src/2d/core/Pipelines2d.js +110 -0
  11. package/src/2d/core/Renderer2d.js +353 -0
  12. package/src/2d/core/Scene2d.js +13 -0
  13. package/src/2d/core/Shape2d.js +13 -0
  14. package/src/2d/core/Uniforms2d.js +13 -0
  15. package/src/2d/geometries/CircleGeometry.js +27 -0
  16. package/src/2d/geometries/Geometry2d.js +108 -0
  17. package/src/2d/geometries/RectGeometry.js +23 -0
  18. package/src/2d/materials/BasicMaterial2d.js +12 -0
  19. package/src/2d/materials/Material2d.js +78 -0
  20. package/src/2d/materials/SpriteMaterial2d.js +28 -0
  21. package/src/2d/shaders/fragments.js +23 -0
  22. package/src/2d/shaders/shared.js +28 -0
  23. package/src/2d/shaders/vertexLayout.js +74 -0
  24. package/src/2d/shaders/vertexStages.js +59 -0
  25. package/src/3d/cameras/Camera.js +66 -0
  26. package/src/3d/cameras/OrthographicCamera.js +35 -0
  27. package/src/3d/cameras/PerspectiveCamera.js +25 -0
  28. package/src/3d/constants.js +18 -0
  29. package/src/3d/controls/DragControls.js +168 -0
  30. package/src/3d/controls/OrbitControls.js +125 -0
  31. package/src/3d/core/DirectionalShadowMap.js +283 -0
  32. package/src/3d/core/GpuResources.js +174 -0
  33. package/src/3d/core/InstanceMatrix.js +55 -0
  34. package/src/3d/core/InstancedBounds.js +114 -0
  35. package/src/3d/core/InstancedMesh.js +124 -0
  36. package/src/3d/core/Mesh.js +26 -0
  37. package/src/3d/core/Object3d.js +101 -0
  38. package/src/3d/core/Pipelines.js +125 -0
  39. package/src/3d/core/RayIntersection.js +184 -0
  40. package/src/3d/core/Raycaster.js +246 -0
  41. package/src/3d/core/Renderer.js +492 -0
  42. package/src/3d/core/Scene.js +13 -0
  43. package/src/3d/core/ShadowPipelines.js +64 -0
  44. package/src/3d/core/Uniforms.js +132 -0
  45. package/src/3d/geometries/BoxGeometry.js +56 -0
  46. package/src/3d/geometries/Geometry.js +97 -0
  47. package/src/3d/geometries/PlaneGeometry.js +24 -0
  48. package/src/3d/geometries/SphereGeometry.js +46 -0
  49. package/src/3d/geometries/WireframeGeometry.js +39 -0
  50. package/src/3d/helpers/GridHelper.js +43 -0
  51. package/src/3d/lights/AmbientLight.js +18 -0
  52. package/src/3d/lights/DirectionalLight.js +26 -0
  53. package/src/3d/lights/DirectionalShadow.js +29 -0
  54. package/src/3d/lights/PointLight.js +23 -0
  55. package/src/3d/materials/BasicMaterial.js +11 -0
  56. package/src/3d/materials/LambertMaterial.js +13 -0
  57. package/src/3d/materials/Material.js +87 -0
  58. package/src/3d/materials/TextureMaterial.js +24 -0
  59. package/src/3d/shaders/fragments.js +34 -0
  60. package/src/3d/shaders/shadows.js +52 -0
  61. package/src/3d/shaders/shared.js +129 -0
  62. package/src/3d/shaders/vertexLayout.js +85 -0
  63. package/src/3d/shaders/vertexStages.js +70 -0
  64. package/src/core/PointerControls.js +258 -0
  65. package/src/core/Texture.js +87 -0
  66. package/src/core/createMaterialPipelineLayouts.js +114 -0
  67. package/src/core/deviceLease.js +67 -0
  68. package/src/core/generateMipmaps.js +115 -0
  69. package/src/core/indexedTriangles.js +53 -0
  70. package/src/core/initWebGpu.js +65 -0
  71. package/src/core/materialResources.js +18 -0
  72. package/src/core/objectGpuResources.js +68 -0
  73. package/src/core/pipelineConstants.js +68 -0
  74. package/src/core/rendererConfig.js +66 -0
  75. package/src/core/uploadTexture.js +57 -0
  76. package/src/index.js +68 -0
  77. package/src/math/Frustum.js +143 -0
  78. package/src/math/Mat3.js +165 -0
  79. package/src/math/Mat4.js +359 -0
  80. package/src/math/Vec2.js +72 -0
  81. package/src/math/Vec3.js +98 -0
  82. package/src/math/color.js +20 -0
@@ -0,0 +1,56 @@
1
+ import { Geometry, VERTEX_SIZE } from './Geometry.js';
2
+
3
+ /**
4
+ * An axis-aligned box centered on the origin, with flat-shaded faces
5
+ * (24 vertices so each face gets its own normals and uvs).
6
+ *
7
+ * Each face is described by the corner where its uvs are (0, 0) plus
8
+ * the two edges the u and v axes run along; the outward normal falls
9
+ * out as edgeU x edgeV, which is also what makes the triangles wind
10
+ * counter-clockwise seen from outside.
11
+ */
12
+ export class BoxGeometry extends Geometry {
13
+ constructor(width = 1, height = 1, depth = 1) {
14
+ const x = width / 2;
15
+ const y = height / 2;
16
+ const z = depth / 2;
17
+
18
+ // prettier-ignore
19
+ const faces = [
20
+ // uv (0,0) corner edge along u edge along v
21
+ { corner: [ x, -y, z], edgeU: [0, 0, -depth], edgeV: [0, height, 0] }, // +X
22
+ { corner: [-x, -y, -z], edgeU: [0, 0, depth], edgeV: [0, height, 0] }, // -X
23
+ { corner: [-x, y, z], edgeU: [width, 0, 0], edgeV: [0, 0, -depth] }, // +Y
24
+ { corner: [-x, -y, -z], edgeU: [width, 0, 0], edgeV: [0, 0, depth] }, // -Y
25
+ { corner: [-x, -y, z], edgeU: [width, 0, 0], edgeV: [0, height, 0] }, // +Z
26
+ { corner: [ x, -y, -z], edgeU: [-width, 0, 0], edgeV: [0, height, 0] }, // -Z
27
+ ];
28
+
29
+ const vertices = [];
30
+ const indices = [];
31
+ for (const { corner, edgeU, edgeV } of faces) {
32
+ let nx = edgeU[1] * edgeV[2] - edgeU[2] * edgeV[1];
33
+ let ny = edgeU[2] * edgeV[0] - edgeU[0] * edgeV[2];
34
+ let nz = edgeU[0] * edgeV[1] - edgeU[1] * edgeV[0];
35
+ const len = Math.hypot(nx, ny, nz);
36
+ nx /= len;
37
+ ny /= len;
38
+ nz /= len;
39
+
40
+ const base = vertices.length / VERTEX_SIZE;
41
+ // The four corners in uv order, then two ccw triangles.
42
+ for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) {
43
+ vertices.push(
44
+ corner[0] + edgeU[0] * u + edgeV[0] * v,
45
+ corner[1] + edgeU[1] * u + edgeV[1] * v,
46
+ corner[2] + edgeU[2] * u + edgeV[2] * v,
47
+ nx, ny, nz,
48
+ u, v,
49
+ );
50
+ }
51
+ indices.push(base, base + 1, base + 2, base, base + 2, base + 3);
52
+ }
53
+
54
+ super(vertices, indices);
55
+ }
56
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Holds the raw mesh data on the CPU side.
3
+ *
4
+ * Vertices are interleaved as 8 floats each:
5
+ * position (x, y, z), normal (x, y, z), uv (u, v)
6
+ * so one GPU vertex buffer with a 32-byte stride serves every attribute.
7
+ *
8
+ * Renderers lazily create GPU buffers the first time the geometry is
9
+ * drawn. `_gpu` holds one cache entry per GPUDevice, so the same CPU
10
+ * geometry can safely be shared by independently initialized renderers.
11
+ */
12
+ export class Geometry {
13
+ /**
14
+ * @param {Float32Array|number[]} vertices interleaved vertex data
15
+ * @param {Uint32Array|number[]} indices triangle indices
16
+ */
17
+ constructor(vertices, indices) {
18
+ this.vertices =
19
+ vertices instanceof Float32Array ? vertices : new Float32Array(vertices);
20
+ this.indices =
21
+ indices instanceof Uint32Array ? indices : new Uint32Array(indices);
22
+ this._gpu = null;
23
+ this._bounds = null;
24
+ this._needsUpdate = false;
25
+ // Each cache entry records the revision it last uploaded. A revision
26
+ // cannot be replaced by one boolean when several devices share this data.
27
+ this._gpuRevision = 0;
28
+ }
29
+
30
+ /**
31
+ * Set to true after editing `vertices` or `indices` in place: the
32
+ * renderer re-uploads the GPU buffers on the next draw and the cached
33
+ * bounds are recomputed. The arrays must keep their length — to
34
+ * change the vertex or index count, make a new geometry.
35
+ */
36
+ get needsUpdate() {
37
+ return this._needsUpdate;
38
+ }
39
+
40
+ set needsUpdate(value) {
41
+ this._needsUpdate = Boolean(value);
42
+ if (this._needsUpdate) {
43
+ this._gpuRevision++;
44
+ this._bounds = null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Local-space axis-aligned bounding box `{ min, max }`, computed from
50
+ * the vertex positions on first access. Used as the raycasting broad phase
51
+ * and for frustum culling.
52
+ */
53
+ get bounds() {
54
+ if (!this._bounds) {
55
+ const min = [Infinity, Infinity, Infinity];
56
+ const max = [-Infinity, -Infinity, -Infinity];
57
+ for (let i = 0; i < this.vertices.length; i += VERTEX_SIZE) {
58
+ for (let a = 0; a < 3; a++) {
59
+ const v = this.vertices[i + a];
60
+ if (v < min[a]) min[a] = v;
61
+ if (v > max[a]) max[a] = v;
62
+ }
63
+ }
64
+ this._bounds = { min, max };
65
+ }
66
+ return this._bounds;
67
+ }
68
+
69
+ get vertexCount() {
70
+ return this.vertices.length / VERTEX_SIZE;
71
+ }
72
+
73
+ get indexCount() {
74
+ return this.indices.length;
75
+ }
76
+
77
+ /**
78
+ * Destroys the GPU vertex/index buffers (if any), releasing the
79
+ * memory right away instead of waiting for GC. Call it when nothing
80
+ * draws this geometry anymore; drawing it again re-uploads.
81
+ */
82
+ dispose() {
83
+ if (this._gpu) {
84
+ for (const { vertexBuffer, indexBuffer } of this._gpu.values()) {
85
+ vertexBuffer.destroy();
86
+ indexBuffer.destroy();
87
+ }
88
+ this._gpu = null;
89
+ }
90
+ return this;
91
+ }
92
+ }
93
+
94
+ /** Number of floats per vertex (3 position + 3 normal + 2 uv). */
95
+ export const VERTEX_SIZE = 8;
96
+ /** Byte stride of one vertex in the GPU buffer. */
97
+ export const VERTEX_STRIDE = VERTEX_SIZE * Float32Array.BYTES_PER_ELEMENT;
@@ -0,0 +1,24 @@
1
+ import { Geometry } from './Geometry.js';
2
+
3
+ /**
4
+ * A flat rectangle lying in the XZ plane, facing up (+Y).
5
+ * Handy as a ground plane.
6
+ */
7
+ export class PlaneGeometry extends Geometry {
8
+ constructor(width = 1, depth = 1) {
9
+ const w = width / 2;
10
+ const d = depth / 2;
11
+
12
+ // prettier-ignore
13
+ const vertices = [
14
+ // position normal uv
15
+ -w, 0, d, 0, 1, 0, 0, 0,
16
+ w, 0, d, 0, 1, 0, 1, 0,
17
+ w, 0, -d, 0, 1, 0, 1, 1,
18
+ -w, 0, -d, 0, 1, 0, 0, 1,
19
+ ];
20
+ const indices = [0, 1, 2, 0, 2, 3];
21
+
22
+ super(vertices, indices);
23
+ }
24
+ }
@@ -0,0 +1,46 @@
1
+ import { Geometry } from './Geometry.js';
2
+
3
+ /**
4
+ * A UV sphere centered on the origin.
5
+ */
6
+ export class SphereGeometry extends Geometry {
7
+ constructor(radius = 1, widthSegments = 24, heightSegments = 16) {
8
+ widthSegments = Math.max(3, Math.floor(widthSegments));
9
+ heightSegments = Math.max(2, Math.floor(heightSegments));
10
+
11
+ const vertices = [];
12
+ const indices = [];
13
+ const grid = [];
14
+
15
+ let index = 0;
16
+ for (let iy = 0; iy <= heightSegments; iy++) {
17
+ const row = [];
18
+ const v = iy / heightSegments;
19
+ for (let ix = 0; ix <= widthSegments; ix++) {
20
+ const u = ix / widthSegments;
21
+
22
+ const x = -radius * Math.cos(u * Math.PI * 2) * Math.sin(v * Math.PI);
23
+ const y = radius * Math.cos(v * Math.PI);
24
+ const z = radius * Math.sin(u * Math.PI * 2) * Math.sin(v * Math.PI);
25
+
26
+ vertices.push(x, y, z, x / radius, y / radius, z / radius, u, 1 - v);
27
+ row.push(index++);
28
+ }
29
+ grid.push(row);
30
+ }
31
+
32
+ for (let iy = 0; iy < heightSegments; iy++) {
33
+ for (let ix = 0; ix < widthSegments; ix++) {
34
+ const a = grid[iy][ix + 1];
35
+ const b = grid[iy][ix];
36
+ const c = grid[iy + 1][ix];
37
+ const d = grid[iy + 1][ix + 1];
38
+
39
+ if (iy !== 0) indices.push(a, b, d);
40
+ if (iy !== heightSegments - 1) indices.push(b, c, d);
41
+ }
42
+ }
43
+
44
+ super(vertices, indices);
45
+ }
46
+ }
@@ -0,0 +1,39 @@
1
+ import { Geometry, VERTEX_SIZE } from './Geometry.js';
2
+
3
+ /**
4
+ * The unique edges of a triangle geometry as line segments — a
5
+ * wireframe. Draw it with a line-list material:
6
+ *
7
+ * new Mesh(
8
+ * new WireframeGeometry(new BoxGeometry()),
9
+ * new BasicMaterial({ color: [0, 0, 0], topology: 'line-list' }),
10
+ * )
11
+ *
12
+ * The source's vertex array is shared, not copied; only new line-list
13
+ * indices are built. Edges are deduplicated by index pair, so seams
14
+ * where a geometry duplicates vertices (BoxGeometry's face corners,
15
+ * SphereGeometry's date line) keep one segment per copy — they draw
16
+ * identically.
17
+ */
18
+ export class WireframeGeometry extends Geometry {
19
+ /** @param {Geometry} geometry a triangle-list geometry */
20
+ constructor(geometry) {
21
+ const source = geometry.indices;
22
+ const vertexCount = geometry.vertices.length / VERTEX_SIZE;
23
+ const indices = [];
24
+ const seen = new Set();
25
+ for (let i = 0; i < source.length; i += 3) {
26
+ const triangle = [source[i], source[i + 1], source[i + 2]];
27
+ for (let e = 0; e < 3; e++) {
28
+ const a = triangle[e];
29
+ const b = triangle[(e + 1) % 3];
30
+ const key = Math.min(a, b) * vertexCount + Math.max(a, b);
31
+ if (!seen.has(key)) {
32
+ seen.add(key);
33
+ indices.push(a, b);
34
+ }
35
+ }
36
+ }
37
+ super(geometry.vertices, indices);
38
+ }
39
+ }
@@ -0,0 +1,43 @@
1
+ import { Mesh } from '../core/Mesh.js';
2
+ import { Geometry } from '../geometries/Geometry.js';
3
+ import { BasicMaterial } from '../materials/BasicMaterial.js';
4
+
5
+ /**
6
+ * A square grid of lines in the XZ plane (y = 0), centered on the
7
+ * origin — the usual ground reference while building a scene. One
8
+ * unlit line-list Mesh, so move, parent or hide it like any other
9
+ * object.
10
+ *
11
+ * Tip: a PlaneGeometry ground at y = 0 z-fights with the grid; nudge
12
+ * the grid up a hair (grid.position.y = 0.001) or the ground down.
13
+ */
14
+ export class GridHelper extends Mesh {
15
+ /**
16
+ * @param {number} size total width/depth in world units
17
+ * @param {number} divisions cells per side
18
+ * @param {number[]} color line color, [r, g, b] in 0..1
19
+ */
20
+ constructor(size = 10, divisions = 10, color = [0.4, 0.4, 0.45]) {
21
+ const half = size / 2;
22
+ const vertices = [];
23
+ const indices = [];
24
+ for (let i = 0; i <= divisions; i++) {
25
+ const t = -half + (i / divisions) * size;
26
+ // One line along X and one along Z per step; 8 floats per vertex
27
+ // (position, unused normal, unused uv) to match VERTEX_STRIDE.
28
+ // prettier-ignore
29
+ vertices.push(
30
+ -half, 0, t, 0, 1, 0, 0, 0,
31
+ half, 0, t, 0, 1, 0, 0, 0,
32
+ t, 0, -half, 0, 1, 0, 0, 0,
33
+ t, 0, half, 0, 1, 0, 0, 0,
34
+ );
35
+ const base = i * 4;
36
+ indices.push(base, base + 1, base + 2, base + 3);
37
+ }
38
+ super(
39
+ new Geometry(vertices, indices),
40
+ new BasicMaterial({ color, topology: 'line-list' }),
41
+ );
42
+ }
43
+ }
@@ -0,0 +1,18 @@
1
+ import { Object3d } from '../core/Object3d.js';
2
+
3
+ /**
4
+ * A light that illuminates every surface equally, regardless of
5
+ * orientation. Used to keep shadows from being pitch black.
6
+ * Multiple AmbientLights in a scene are added together.
7
+ */
8
+ export class AmbientLight extends Object3d {
9
+ /**
10
+ * @param {number[]} color [r, g, b] in the 0..1 range
11
+ * @param {number} intensity brightness multiplier
12
+ */
13
+ constructor(color = [1, 1, 1], intensity = 0.2) {
14
+ super();
15
+ this.color = color;
16
+ this.intensity = intensity;
17
+ }
18
+ }
@@ -0,0 +1,26 @@
1
+ import { Object3d } from '../core/Object3d.js';
2
+ import { Vec3 } from '../../math/Vec3.js';
3
+ import { DirectionalShadow } from './DirectionalShadow.js';
4
+
5
+ /**
6
+ * A light that shines in one direction from infinitely far away,
7
+ * like the sun. The renderer uses the first DirectionalLight it
8
+ * finds in the scene.
9
+ */
10
+ export class DirectionalLight extends Object3d {
11
+ /**
12
+ * @param {number[]} color [r, g, b] in the 0..1 range
13
+ * @param {number} intensity brightness multiplier
14
+ */
15
+ constructor(color = [1, 1, 1], intensity = 1) {
16
+ super();
17
+ this.color = color;
18
+ this.intensity = intensity;
19
+ /** The direction the light travels in (it gets normalized by the renderer). */
20
+ this.direction = new Vec3(-1, -1, -1);
21
+ /** Enable the renderer's directional shadow pass for this light. */
22
+ this.castShadow = false;
23
+ /** Shadow-map resolution, bias and orthographic camera configuration. */
24
+ this.shadow = new DirectionalShadow();
25
+ }
26
+ }
@@ -0,0 +1,29 @@
1
+ import { OrthographicCamera } from '../cameras/OrthographicCamera.js';
2
+
3
+ export const DEFAULT_SHADOW_MAP_SIZE = 1024;
4
+ export const DEFAULT_SHADOW_CAMERA_SIZE = 10;
5
+ export const DEFAULT_SHADOW_NEAR = 0.1;
6
+ export const DEFAULT_SHADOW_FAR = 50;
7
+ export const DEFAULT_SHADOW_BIAS = 0.001;
8
+ export const DEFAULT_SHADOW_NORMAL_BIAS = 0.02;
9
+
10
+ /**
11
+ * CPU-side configuration for one directional-light shadow map.
12
+ *
13
+ * `camera.size` is the half-height of the square world-space area covered by
14
+ * the map. Aim that area with `camera.lookAt(...)`; the renderer positions the
15
+ * camera automatically from the light direction and its near/far range.
16
+ */
17
+ export class DirectionalShadow {
18
+ constructor() {
19
+ this.mapSize = DEFAULT_SHADOW_MAP_SIZE;
20
+ this.bias = DEFAULT_SHADOW_BIAS;
21
+ this.normalBias = DEFAULT_SHADOW_NORMAL_BIAS;
22
+ this.camera = new OrthographicCamera(
23
+ DEFAULT_SHADOW_CAMERA_SIZE,
24
+ 1,
25
+ DEFAULT_SHADOW_NEAR,
26
+ DEFAULT_SHADOW_FAR,
27
+ );
28
+ }
29
+ }
@@ -0,0 +1,23 @@
1
+ import { Object3d } from '../core/Object3d.js';
2
+
3
+ /**
4
+ * A light that radiates from a point in all directions, fading with
5
+ * distance — a lamp. It sits in the scene graph like any object: its
6
+ * world position (parenting included) is where the light comes from.
7
+ *
8
+ * Brightness falls off as 1 / (1 + distance²), so `intensity` is the
9
+ * brightness right at the light and roughly a tenth of it three units
10
+ * away. The renderer uses the first MAX_POINT_LIGHTS (4) visible
11
+ * PointLights it finds in the scene; extras are ignored.
12
+ */
13
+ export class PointLight extends Object3d {
14
+ /**
15
+ * @param {number[]} color [r, g, b] in the 0..1 range
16
+ * @param {number} intensity brightness multiplier
17
+ */
18
+ constructor(color = [1, 1, 1], intensity = 1) {
19
+ super();
20
+ this.color = color;
21
+ this.intensity = intensity;
22
+ }
23
+ }
@@ -0,0 +1,11 @@
1
+ import { Material } from './Material.js';
2
+ import { BASIC_FRAGMENT_SHADER } from '../shaders/fragments.js';
3
+
4
+ /**
5
+ * An unlit material: renders a flat color, ignoring all lights.
6
+ */
7
+ export class BasicMaterial extends Material {
8
+ get fragmentShader() {
9
+ return BASIC_FRAGMENT_SHADER;
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ import { Material } from './Material.js';
2
+ import { LAMBERT_FRAGMENT_SHADER } from '../shaders/fragments.js';
3
+
4
+ /**
5
+ * A simple diffuse (Lambertian) material: brightness depends on the
6
+ * angle between the surface and the scene's lights (the
7
+ * DirectionalLight and any PointLights), plus a flat ambient term.
8
+ */
9
+ export class LambertMaterial extends Material {
10
+ get fragmentShader() {
11
+ return LAMBERT_FRAGMENT_SHADER;
12
+ }
13
+ }
@@ -0,0 +1,87 @@
1
+ import {
2
+ DEFAULT_CULL_MODE_3D,
3
+ DEFAULT_FRONT_FACE,
4
+ DEFAULT_PRIMITIVE_TOPOLOGY,
5
+ } from '../../core/pipelineConstants.js';
6
+ import { MAX_POINT_LIGHTS } from '../constants.js';
7
+ import {
8
+ INSTANCED_MESH_SHADER_PREFIX,
9
+ MESH_SHADER_PREFIX,
10
+ } from '../shaders/vertexStages.js';
11
+
12
+ /**
13
+ * Base class for materials. A material owns per-object parameters and
14
+ * fixed-function pipeline state; reusable WGSL stages live in `3d/shaders`.
15
+ *
16
+ * Every shader shares the same uniform interface:
17
+ * @group(0) — per-frame data (camera + lights), owned by the renderer
18
+ * @group(1) — per-object data (transforms + color)
19
+ * @group(2) — renderer-owned directional shadow map and uniforms
20
+ * The renderer caches pipelines by composed WGSL source and pipeline state,
21
+ * so materials with the same shader share one pipeline while custom material
22
+ * instances may still provide different fragment stages safely.
23
+ */
24
+ export class Material {
25
+ /**
26
+ * @param {object} [options]
27
+ * @param {number[]} [options.color] [r, g, b] in 0..1
28
+ * @param {Texture} [options.map] texture the shader can sample at the
29
+ * mesh's uvs — only used when `usesMap` is true
30
+ * @param {boolean} [options.usesMap] whether the fragment shader declares
31
+ * the map and sampler bindings. It defaults to whether `map` was supplied
32
+ * for compatibility; custom material subclasses should set it explicitly.
33
+ * It is fixed for the material's lifetime so replacing or clearing `map`
34
+ * cannot silently change its pipeline layout
35
+ * @param {GPUPrimitiveTopology} [options.topology]
36
+ * 'triangle-list' (default), 'triangle-strip', 'line-list',
37
+ * 'line-strip' or 'point-list'
38
+ * @param {GPUCullMode} [options.cullMode] 'back' (default), 'front' or
39
+ * 'none'; only applies to triangle topologies
40
+ * @param {GPUFrontFace} [options.frontFace] 'ccw' (default) or 'cw'
41
+ * @param {boolean} [options.transparent] alpha-blend this material: the
42
+ * renderer draws transparent meshes after all opaque ones, sorted
43
+ * back-to-front, and they stop writing the depth buffer. Combine
44
+ * with a color alpha below 1 (or a texture with alpha)
45
+ */
46
+ constructor({
47
+ color = [1, 1, 1],
48
+ map = null,
49
+ usesMap = map !== null,
50
+ topology = DEFAULT_PRIMITIVE_TOPOLOGY,
51
+ cullMode = DEFAULT_CULL_MODE_3D,
52
+ frontFace = DEFAULT_FRONT_FACE,
53
+ transparent = false,
54
+ } = {}) {
55
+ this.color = color;
56
+ this.map = map;
57
+ Object.defineProperty(this, 'usesMap', {
58
+ value: Boolean(usesMap),
59
+ enumerable: true,
60
+ });
61
+ this.topology = topology;
62
+ this.cullMode = cullMode;
63
+ this.frontFace = frontFace;
64
+ this.transparent = transparent;
65
+ }
66
+
67
+ /** Full WGSL source with `vs` and `fs` entry points. */
68
+ get shaderCode() {
69
+ return Material.SHARED_WGSL + this.fragmentShader;
70
+ }
71
+
72
+ /** Full WGSL source whose vertex stage reads per-instance data. */
73
+ get instancedShaderCode() {
74
+ return Material.INSTANCED_WGSL + this.fragmentShader;
75
+ }
76
+
77
+ /** Fragment-stage WGSL supplied by a concrete material. */
78
+ get fragmentShader() {
79
+ throw new Error('Material subclasses must implement fragmentShader');
80
+ }
81
+
82
+ // Mutable for compatibility with code that customized these fields directly.
83
+ static SHARED_WGSL = MESH_SHADER_PREFIX;
84
+ static INSTANCED_WGSL = INSTANCED_MESH_SHADER_PREFIX;
85
+ }
86
+
87
+ export { MAX_POINT_LIGHTS };
@@ -0,0 +1,24 @@
1
+ import { Material } from './Material.js';
2
+ import { TEXTURE_FRAGMENT_SHADER } from '../shaders/fragments.js';
3
+
4
+ /**
5
+ * A diffuse (Lambertian) material that gets its surface color from a
6
+ * texture sampled at the mesh's uvs, tinted by `color` — LambertMaterial
7
+ * with a `map`. Requires the map: the shader's texture bindings must
8
+ * exist when the pipeline is created.
9
+ */
10
+ export class TextureMaterial extends Material {
11
+ /** @param {{map: Texture, color?: number[]}} options see Material for the rest */
12
+ constructor(options = {}) {
13
+ super({ ...options, usesMap: true });
14
+ // Compatibility alias retained for existing material introspection.
15
+ this.requiresMap = true;
16
+ if (!this.map) {
17
+ throw new Error('TextureMaterial requires a `map` texture');
18
+ }
19
+ }
20
+
21
+ get fragmentShader() {
22
+ return TEXTURE_FRAGMENT_SHADER;
23
+ }
24
+ }
@@ -0,0 +1,34 @@
1
+ import {
2
+ SHADER_BIND_GROUP,
3
+ SHADER_BINDING,
4
+ } from '../../core/pipelineConstants.js';
5
+
6
+ export const BASIC_FRAGMENT_SHADER = /* wgsl */ `
7
+ @fragment
8
+ fn fs(input: VertexOut) -> @location(0) vec4f {
9
+ return objectColor(input);
10
+ }
11
+ `;
12
+
13
+ export const LAMBERT_FRAGMENT_SHADER = /* wgsl */ `
14
+ @fragment
15
+ fn fs(input: VertexOut) -> @location(0) vec4f {
16
+ let base = objectColor(input);
17
+ let lighting = diffuseLighting(normalize(input.worldNormal), input.worldPosition);
18
+ return vec4f(base.rgb * lighting, base.a);
19
+ }
20
+ `;
21
+
22
+ export const TEXTURE_FRAGMENT_SHADER = /* wgsl */ `
23
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.map})
24
+ var uMap: texture_2d<f32>;
25
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.sampler})
26
+ var uMapSampler: sampler;
27
+
28
+ @fragment
29
+ fn fs(input: VertexOut) -> @location(0) vec4f {
30
+ let base = textureSample(uMap, uMapSampler, input.uv) * objectColor(input);
31
+ let lighting = diffuseLighting(normalize(input.worldNormal), input.worldPosition);
32
+ return vec4f(base.rgb * lighting, base.a);
33
+ }
34
+ `;
@@ -0,0 +1,52 @@
1
+ import {
2
+ SHADER_BIND_GROUP,
3
+ SHADER_BINDING,
4
+ } from '../../core/pipelineConstants.js';
5
+ import { VERTEX_ATTRIBUTE } from './vertexLayout.js';
6
+ import {
7
+ OBJECT_UNIFORM_WGSL,
8
+ SHADOW_UNIFORM_WGSL,
9
+ } from './shared.js';
10
+
11
+ /** Vertex-only shader used by regular and instanced shadow casters. */
12
+ export const DIRECTIONAL_SHADOW_SHADER = /* wgsl */ `
13
+ ${SHADOW_UNIFORM_WGSL}
14
+ ${OBJECT_UNIFORM_WGSL}
15
+
16
+ @group(${SHADER_BIND_GROUP.frame}) @binding(${SHADER_BINDING.uniforms})
17
+ var<uniform> uShadow: ShadowUniforms;
18
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.uniforms})
19
+ var<uniform> uObject: ObjectUniforms;
20
+
21
+ struct ShadowVertexIn {
22
+ @location(${VERTEX_ATTRIBUTE.position}) position: vec3f,
23
+ };
24
+
25
+ struct ShadowInstanceIn {
26
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix0}) im0: vec4f,
27
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix1}) im1: vec4f,
28
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix2}) im2: vec4f,
29
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix3}) im3: vec4f,
30
+ };
31
+
32
+ @vertex
33
+ fn vsShadow(input: ShadowVertexIn) -> @builtin(position) vec4f {
34
+ return uShadow.viewProjection * uObject.model
35
+ * vec4f(input.position, 1.0);
36
+ }
37
+
38
+ @vertex
39
+ fn vsShadowInstanced(
40
+ input: ShadowVertexIn,
41
+ instance: ShadowInstanceIn,
42
+ ) -> @builtin(position) vec4f {
43
+ let instanceMatrix = mat4x4f(
44
+ instance.im0,
45
+ instance.im1,
46
+ instance.im2,
47
+ instance.im3,
48
+ );
49
+ return uShadow.viewProjection * uObject.model * instanceMatrix
50
+ * vec4f(input.position, 1.0);
51
+ }
52
+ `;