3d-spinner 0.9.0 → 0.9.2

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 (29) hide show
  1. package/README.md +49 -8
  2. package/dist/animations/object-motion.d.ts +4 -1
  3. package/dist/animations/object-motion.js +2 -1
  4. package/dist/animations/spin.d.ts +4 -1
  5. package/dist/animations/spin.js +2 -1
  6. package/dist/cjs/animations/object-motion.cjs +1368 -0
  7. package/dist/cjs/animations/spin.cjs +1299 -0
  8. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +1324 -0
  9. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +58 -0
  10. package/dist/cjs/engines/little-tween-engine/little-tween-engine.cjs +279 -0
  11. package/dist/cjs/index.cjs +120 -0
  12. package/dist/cjs/motion/motion.cjs +140 -0
  13. package/dist/cjs/motion/transitions.cjs +80 -0
  14. package/dist/engines/little-3d-engine/core/mesh.d.ts +18 -0
  15. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +10 -3
  16. package/dist/engines/little-3d-engine/little-3d-engine.js +12 -8
  17. package/dist/engines/little-3d-engine/loaders/obj.d.ts +2 -2
  18. package/dist/engines/little-3d-engine/loaders/obj.js +2 -2
  19. package/dist/engines/little-3d-engine/renderer.d.ts +15 -1
  20. package/dist/engines/little-3d-engine/renderer.js +36 -0
  21. package/dist/engines/little-3d-engine/renderers/canvas2d.d.ts +1 -1
  22. package/dist/engines/little-3d-engine/renderers/canvas2d.js +22 -2
  23. package/dist/engines/little-3d-engine/renderers/webgl.d.ts +1 -1
  24. package/dist/engines/little-3d-engine/renderers/webgl.js +38 -1
  25. package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +3 -1
  26. package/dist/engines/little-3d-engine/renderers/webgpu.js +70 -17
  27. package/dist/umd/spinner.global.js +2408 -0
  28. package/dist/umd/spinner.global.min.js +58 -0
  29. package/package.json +26 -11
@@ -1,6 +1,6 @@
1
1
  import { type CameraOptions } from "./core/camera.js";
2
2
  import { type LightOptions } from "./core/light.js";
3
- import { type Mesh, type Transform } from "./core/mesh.js";
3
+ import { type Mesh, type Transform, type Transparency } from "./core/mesh.js";
4
4
  import { type Backend } from "./renderer.js";
5
5
  /** Options for {@link Little3dEngine}. */
6
6
  export interface Little3dEngineOptions {
@@ -15,9 +15,15 @@ export interface Little3dEngineOptions {
15
15
  export interface MeshHandle {
16
16
  readonly mesh: Mesh;
17
17
  readonly transform: Transform;
18
+ /** Optional per-instance transparency. Mutate or replace it between frames. */
19
+ transparency?: Transparency;
18
20
  /** Remove this mesh from the scene. */
19
21
  remove(): void;
20
22
  }
23
+ /** Initial state for one mesh instance. */
24
+ export interface MeshInstanceOptions extends Partial<Transform> {
25
+ transparency?: Transparency;
26
+ }
21
27
  /**
22
28
  * A minimal software/hardware 3D engine. It projects colored meshes with flat
23
29
  * directional lighting through a swappable {@link Backend} renderer. Mount it
@@ -47,7 +53,7 @@ export declare class Little3dEngine {
47
53
  */
48
54
  mount(target: HTMLElement): Promise<void>;
49
55
  /** Add a mesh to the scene and return a handle for animating it. */
50
- add(mesh: Mesh, init?: Partial<Transform>): MeshHandle;
56
+ add(mesh: Mesh, init?: MeshInstanceOptions): MeshHandle;
51
57
  private resize;
52
58
  /** Draw a single frame from the current scene state. */
53
59
  render(): void;
@@ -69,7 +75,8 @@ export { icosphere } from "./shapes/icosphere.js";
69
75
  export { octaSphere } from "./shapes/octa-sphere.js";
70
76
  export { cubeSphere } from "./shapes/cube-sphere.js";
71
77
  export { expandToTriangles } from "./core/geometry.js";
72
- export type { Mesh, Face, Transform } from "./core/mesh.js";
78
+ export type { Mesh, Face, Transform, Transparency, OneSidedTransparency, TwoSidedTransparency, } from "./core/mesh.js";
73
79
  export { transform } from "./core/mesh.js";
74
80
  export type { Backend, Renderer, RenderFrame, RenderItem, RendererOptions } from "./renderer.js";
81
+ export { orderRenderItems } from "./renderer.js";
75
82
  export { type Vec3, vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
@@ -1,8 +1,8 @@
1
1
  import { Camera } from "./core/camera.js";
2
2
  import { Light } from "./core/light.js";
3
3
  import { multiply, rotationX, rotationY, rotationZ, scaleMatrix, translation, } from "./core/math.js";
4
- import { transform as makeTransform } from "./core/mesh.js";
5
- import { createRenderer } from "./renderer.js";
4
+ import { transform as makeTransform, } from "./core/mesh.js";
5
+ import { createRenderer, orderRenderItems, } from "./renderer.js";
6
6
  function modelMatrix(t) {
7
7
  const rotation = multiply(rotationZ(t.rotation.z), multiply(rotationY(t.rotation.y), rotationX(t.rotation.x)));
8
8
  return multiply(translation(t.position.x, t.position.y, t.position.z), multiply(rotation, scaleMatrix(t.scale)));
@@ -75,17 +75,18 @@ export class Little3dEngine {
75
75
  }
76
76
  /** Add a mesh to the scene and return a handle for animating it. */
77
77
  add(mesh, init) {
78
- const entry = { mesh, transform: makeTransform(init) };
79
- this.scene.push(entry);
80
- return {
78
+ const entry = {
81
79
  mesh,
82
- transform: entry.transform,
80
+ transform: makeTransform(init),
81
+ transparency: init?.transparency,
83
82
  remove: () => {
84
83
  const i = this.scene.indexOf(entry);
85
84
  if (i >= 0)
86
85
  this.scene.splice(i, 1);
87
86
  },
88
87
  };
88
+ this.scene.push(entry);
89
+ return entry;
89
90
  }
90
91
  resize() {
91
92
  const canvas = this.canvas;
@@ -109,11 +110,13 @@ export class Little3dEngine {
109
110
  const items = this.scene.map((entry) => ({
110
111
  mesh: entry.mesh,
111
112
  model: modelMatrix(entry.transform),
113
+ transparency: entry.transparency,
112
114
  }));
115
+ const eye = this.camera.options.position;
113
116
  this.renderer.render({
114
- items,
117
+ items: orderRenderItems(items, eye),
115
118
  viewProjection: this.camera.viewProjection(width / height),
116
- eye: this.camera.options.position,
119
+ eye,
117
120
  light: this.light.params,
118
121
  width,
119
122
  height,
@@ -164,4 +167,5 @@ export { octaSphere } from "./shapes/octa-sphere.js";
164
167
  export { cubeSphere } from "./shapes/cube-sphere.js";
165
168
  export { expandToTriangles } from "./core/geometry.js";
166
169
  export { transform } from "./core/mesh.js";
170
+ export { orderRenderItems } from "./renderer.js";
167
171
  export { vec3, subtract, cross, dot, scale, normalize, } from "./core/math.js";
@@ -15,8 +15,8 @@ export interface ObjOptions {
15
15
  * `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
16
16
  * Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
17
17
  * computes a flat normal per face - as are groups, materials, and other
18
- * statements. Face winding is preserved, so models should be wound CCW as seen
19
- * from outside for correct backface culling and shading.
18
+ * statements. Face winding is preserved as-is; the engine expects CCW
19
+ * winding as seen from outside.
20
20
  *
21
21
  * @param text Contents of an `.obj` file.
22
22
  * @param options Color palette to apply to the faces.
@@ -10,8 +10,8 @@ function resolveIndex(token, vertexCount) {
10
10
  * `v`, `v/vt`, `v/vt/vn`, or `v//vn` form, with 1-based or negative indices).
11
11
  * Normals (`vn`) and texture coordinates (`vt`) are ignored - the engine
12
12
  * computes a flat normal per face - as are groups, materials, and other
13
- * statements. Face winding is preserved, so models should be wound CCW as seen
14
- * from outside for correct backface culling and shading.
13
+ * statements. Face winding is preserved as-is; the engine expects CCW
14
+ * winding as seen from outside.
15
15
  *
16
16
  * @param text Contents of an `.obj` file.
17
17
  * @param options Color palette to apply to the faces.
@@ -1,13 +1,26 @@
1
1
  import type { LightParams } from "./core/light.js";
2
2
  import type { Mat4, Vec3 } from "./core/math.js";
3
- import type { Mesh } from "./core/mesh.js";
3
+ import type { Mesh, Transparency, TwoSidedTransparency } from "./core/mesh.js";
4
4
  /** Rendering backend. Each is loaded on demand; unused ones are never fetched. */
5
5
  export type Backend = "canvas2d" | "webgl" | "webgpu";
6
6
  /** A mesh plus its world transform, ready to draw. */
7
7
  export interface RenderItem {
8
8
  mesh: Mesh;
9
9
  model: Mat4;
10
+ transparency?: Transparency;
10
11
  }
12
+ export declare const DEFAULT_ONE_SIDED_OPACITY = 0.35;
13
+ export declare const DEFAULT_BACK_OPACITY = 0.84;
14
+ export declare const DEFAULT_FRONT_OPACITY = 0.56;
15
+ /** Clamp an optional opacity to the range accepted by rendering backends. */
16
+ export declare function opacity(value: number | undefined, fallback: number): number;
17
+ /** Resolve two-sided defaults, shorthand, and explicit per-side overrides. */
18
+ export declare function resolveTwoSidedOpacity(transparency: TwoSidedTransparency): {
19
+ front: number;
20
+ back: number;
21
+ };
22
+ /** Draw opaque instances first, then transparent instances from farthest to nearest. */
23
+ export declare function orderRenderItems(items: ReadonlyArray<RenderItem>, eye: Vec3): RenderItem[];
11
24
  /** Everything a renderer needs to draw one frame. */
12
25
  export interface RenderFrame {
13
26
  items: ReadonlyArray<RenderItem>;
@@ -28,6 +41,7 @@ export interface RendererOptions {
28
41
  /**
29
42
  * A pluggable drawing backend. The engine owns the canvas and sizing; a
30
43
  * renderer only initializes its context, reacts to resizes, and draws frames.
44
+ * Renderer-specific features can produce intentional visual differences.
31
45
  */
32
46
  export interface Renderer {
33
47
  init(canvas: HTMLCanvasElement): void | Promise<void>;
@@ -1,3 +1,39 @@
1
+ export const DEFAULT_ONE_SIDED_OPACITY = 0.35;
2
+ export const DEFAULT_BACK_OPACITY = 0.84;
3
+ export const DEFAULT_FRONT_OPACITY = 0.56;
4
+ /** Clamp an optional opacity to the range accepted by rendering backends. */
5
+ export function opacity(value, fallback) {
6
+ return Math.max(0, Math.min(1, value ?? fallback));
7
+ }
8
+ /** Resolve two-sided defaults, shorthand, and explicit per-side overrides. */
9
+ export function resolveTwoSidedOpacity(transparency) {
10
+ const front = opacity(transparency.frontOpacity ?? transparency.opacity, DEFAULT_FRONT_OPACITY);
11
+ const backFallback = transparency.opacity === undefined
12
+ ? DEFAULT_BACK_OPACITY
13
+ : front * (2 / 3);
14
+ return {
15
+ front,
16
+ back: opacity(transparency.backOpacity, backFallback),
17
+ };
18
+ }
19
+ /** Draw opaque instances first, then transparent instances from farthest to nearest. */
20
+ export function orderRenderItems(items, eye) {
21
+ const opaque = [];
22
+ const transparent = [];
23
+ for (const item of items) {
24
+ (item.transparency ? transparent : opaque).push(item);
25
+ }
26
+ transparent.sort((a, b) => {
27
+ const ax = a.model[12] - eye.x;
28
+ const ay = a.model[13] - eye.y;
29
+ const az = a.model[14] - eye.z;
30
+ const bx = b.model[12] - eye.x;
31
+ const by = b.model[13] - eye.y;
32
+ const bz = b.model[14] - eye.z;
33
+ return bx * bx + by * by + bz * bz - (ax * ax + ay * ay + az * az);
34
+ });
35
+ return opaque.concat(transparent);
36
+ }
1
37
  /**
2
38
  * Load and construct a renderer for `backend`. Each backend lives in its own
3
39
  * module and is pulled in with a dynamic `import()`, so the bytes for the
@@ -1,4 +1,4 @@
1
- import type { Renderer, RenderFrame, RendererOptions } from "../renderer.js";
1
+ import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
2
2
  /** Software renderer: projects geometry on the CPU and fills 2D polygons. */
3
3
  export declare class Canvas2DRenderer implements Renderer {
4
4
  private readonly options;
@@ -1,5 +1,6 @@
1
1
  import { shadeColor } from "../core/light.js";
2
2
  import { dot, cross, normalize, subtract, transformAffine, transformPoint } from "../core/math.js";
3
+ import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
3
4
  /** Software renderer: projects geometry on the CPU and fills 2D polygons. */
4
5
  export class Canvas2DRenderer {
5
6
  constructor(options = {}) {
@@ -27,13 +28,25 @@ export class Canvas2DRenderer {
27
28
  const polygons = [];
28
29
  for (const item of frame.items) {
29
30
  const world = item.mesh.vertices.map((v) => transformAffine(item.model, v));
31
+ const twoSidedOpacity = item.transparency?.mode === "two-sided"
32
+ ? resolveTwoSidedOpacity(item.transparency)
33
+ : undefined;
30
34
  for (const face of item.mesh.faces) {
31
35
  const a = world[face.indices[0]];
32
36
  const b = world[face.indices[1]];
33
37
  const c = world[face.indices[2]];
34
38
  const normal = normalize(cross(subtract(b, a), subtract(c, a)));
35
- if (dot(normal, subtract(frame.eye, a)) <= 0)
39
+ const frontFacing = dot(normal, subtract(frame.eye, a)) > 0;
40
+ const transparency = item.transparency;
41
+ if (!frontFacing && transparency?.mode !== "two-sided")
36
42
  continue;
43
+ let faceOpacity = 1;
44
+ if (transparency?.mode === "one-sided") {
45
+ faceOpacity = opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
46
+ }
47
+ else if (twoSidedOpacity) {
48
+ faceOpacity = frontFacing ? twoSidedOpacity.front : twoSidedOpacity.back;
49
+ }
37
50
  const points = face.indices.map((i) => {
38
51
  const ndc = transformPoint(frame.viewProjection, world[i]);
39
52
  return {
@@ -47,7 +60,12 @@ export class Canvas2DRenderer {
47
60
  depth += dot(d, d);
48
61
  }
49
62
  depth /= face.indices.length;
50
- polygons.push({ points, color: shadeColor(normal, face.color, frame.light), depth });
63
+ polygons.push({
64
+ points,
65
+ color: shadeColor(normal, face.color, frame.light),
66
+ depth,
67
+ opacity: faceOpacity,
68
+ });
51
69
  }
52
70
  }
53
71
  polygons.sort((p, q) => q.depth - p.depth);
@@ -63,9 +81,11 @@ export class Canvas2DRenderer {
63
81
  ctx.fillStyle = poly.color;
64
82
  ctx.strokeStyle = poly.color;
65
83
  ctx.lineWidth = 1;
84
+ ctx.globalAlpha = poly.opacity;
66
85
  ctx.fill();
67
86
  ctx.stroke();
68
87
  }
88
+ ctx.globalAlpha = 1;
69
89
  }
70
90
  destroy() {
71
91
  this.ctx = undefined;
@@ -1,4 +1,4 @@
1
- import type { Renderer, RenderFrame, RendererOptions } from "../renderer.js";
1
+ import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
2
2
  /** Hardware renderer using WebGL2: GPU transforms with a real depth buffer. */
3
3
  export declare class WebGLRenderer implements Renderer {
4
4
  private gl?;
@@ -1,4 +1,5 @@
1
1
  import { expandToTriangles, parseColor } from "../core/geometry.js";
2
+ import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
2
3
  const VERTEX_SHADER = `#version 300 es
3
4
  in vec3 aPos;
4
5
  in vec3 aNormal;
@@ -19,11 +20,12 @@ in vec3 vColor;
19
20
  uniform vec3 uToLight;
20
21
  uniform float uIntensity;
21
22
  uniform float uAmbient;
23
+ uniform float uOpacity;
22
24
  out vec4 fragColor;
23
25
  void main() {
24
26
  float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
25
27
  float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
26
- fragColor = vec4(vColor * brightness, 1.0);
28
+ fragColor = vec4(vColor * brightness, uOpacity);
27
29
  }`;
28
30
  function compile(gl, type, source) {
29
31
  const shader = gl.createShader(type);
@@ -71,6 +73,7 @@ export class WebGLRenderer {
71
73
  uToLight: gl.getUniformLocation(this.program, "uToLight"),
72
74
  uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
73
75
  uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
76
+ uOpacity: gl.getUniformLocation(this.program, "uOpacity"),
74
77
  };
75
78
  gl.enable(gl.DEPTH_TEST);
76
79
  gl.enable(gl.CULL_FACE);
@@ -122,12 +125,46 @@ export class WebGLRenderer {
122
125
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
123
126
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
124
127
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
128
+ gl.disable(gl.BLEND);
129
+ gl.depthMask(true);
130
+ gl.cullFace(gl.BACK);
125
131
  for (const item of frame.items) {
132
+ if (item.transparency)
133
+ continue;
126
134
  const mesh = this.buffers(item.mesh);
127
135
  gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
136
+ gl.uniform1f(loc.uOpacity, 1);
128
137
  gl.bindVertexArray(mesh.vao);
129
138
  gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
130
139
  }
140
+ gl.enable(gl.BLEND);
141
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
142
+ gl.depthMask(false);
143
+ for (const item of frame.items) {
144
+ const transparency = item.transparency;
145
+ if (!transparency)
146
+ continue;
147
+ const mesh = this.buffers(item.mesh);
148
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
149
+ gl.bindVertexArray(mesh.vao);
150
+ if (transparency.mode === "two-sided") {
151
+ const resolved = resolveTwoSidedOpacity(transparency);
152
+ gl.cullFace(gl.FRONT);
153
+ gl.uniform1f(loc.uOpacity, resolved.back);
154
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
155
+ gl.cullFace(gl.BACK);
156
+ gl.uniform1f(loc.uOpacity, resolved.front);
157
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
158
+ }
159
+ else {
160
+ gl.cullFace(gl.BACK);
161
+ gl.uniform1f(loc.uOpacity, opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY));
162
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
163
+ }
164
+ }
165
+ gl.depthMask(true);
166
+ gl.disable(gl.BLEND);
167
+ gl.cullFace(gl.BACK);
131
168
  gl.bindVertexArray(null);
132
169
  }
133
170
  destroy() {
@@ -1,10 +1,12 @@
1
- import type { Renderer, RenderFrame, RendererOptions } from "../renderer.js";
1
+ import { type Renderer, type RenderFrame, type RendererOptions } from "../renderer.js";
2
2
  /** Hardware renderer using WebGPU: GPU transforms with a real depth buffer. */
3
3
  export declare class WebGPURenderer implements Renderer {
4
4
  private canvas?;
5
5
  private device;
6
6
  private context;
7
7
  private pipeline;
8
+ private transparentBackPipeline;
9
+ private transparentFrontPipeline;
8
10
  private uniformBuffer;
9
11
  private uniformCapacity;
10
12
  private depthTexture;
@@ -1,5 +1,6 @@
1
1
  import { expandToTriangles, parseColor } from "../core/geometry.js";
2
2
  import { multiply } from "../core/math.js";
3
+ import { DEFAULT_ONE_SIDED_OPACITY, opacity, resolveTwoSidedOpacity, } from "../renderer.js";
3
4
  const WGSL = `
4
5
  struct Uniforms {
5
6
  viewProj: mat4x4<f32>,
@@ -29,7 +30,7 @@ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2)
29
30
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
30
31
  let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
31
32
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
32
- return vec4<f32>(in.color * brightness, 1.0);
33
+ return vec4<f32>(in.color * brightness, u.params.z);
33
34
  }
34
35
  `;
35
36
  // Maps OpenGL clip space (z in -1..1) to WebGPU clip space (z in 0..1).
@@ -84,17 +85,37 @@ export class WebGPURenderer {
84
85
  arrayStride: 12,
85
86
  attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }],
86
87
  });
87
- this.pipeline = device.createRenderPipeline({
88
- layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
88
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
89
+ const blend = {
90
+ color: {
91
+ srcFactor: "src-alpha",
92
+ dstFactor: "one-minus-src-alpha",
93
+ operation: "add",
94
+ },
95
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
96
+ };
97
+ const pipeline = (cullMode, transparent) => device.createRenderPipeline({
98
+ layout: pipelineLayout,
89
99
  vertex: {
90
100
  module,
91
101
  entryPoint: "vs",
92
102
  buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)],
93
103
  },
94
- fragment: { module, entryPoint: "fs", targets: [{ format }] },
95
- primitive: { topology: "triangle-list", cullMode: "back", frontFace: "ccw" },
96
- depthStencil: { format: "depth24plus", depthWriteEnabled: true, depthCompare: "less" },
104
+ fragment: {
105
+ module,
106
+ entryPoint: "fs",
107
+ targets: [{ format, ...(transparent ? { blend } : {}) }],
108
+ },
109
+ primitive: { topology: "triangle-list", cullMode, frontFace: "ccw" },
110
+ depthStencil: {
111
+ format: "depth24plus",
112
+ depthWriteEnabled: !transparent,
113
+ depthCompare: "less",
114
+ },
97
115
  });
116
+ this.pipeline = pipeline("back", false);
117
+ this.transparentBackPipeline = pipeline("front", true);
118
+ this.transparentFrontPipeline = pipeline("back", true);
98
119
  this.canvas = canvas;
99
120
  this.device = device;
100
121
  this.context = context;
@@ -139,15 +160,15 @@ export class WebGPURenderer {
139
160
  this.cache.set(mesh, result);
140
161
  return result;
141
162
  }
142
- ensureUniformCapacity(items) {
143
- if (items <= this.uniformCapacity && this.uniformBuffer)
163
+ ensureUniformCapacity(draws) {
164
+ if (draws <= this.uniformCapacity && this.uniformBuffer)
144
165
  return;
145
166
  this.uniformBuffer?.destroy?.();
146
167
  this.uniformBuffer = this.device.createBuffer({
147
- size: Math.max(1, items) * UNIFORM_STRIDE,
168
+ size: Math.max(1, draws) * UNIFORM_STRIDE,
148
169
  usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST,
149
170
  });
150
- this.uniformCapacity = items;
171
+ this.uniformCapacity = draws;
151
172
  }
152
173
  render(frame) {
153
174
  if (this.destroyed || !this.device || !this.context || !this.pipeline)
@@ -155,7 +176,37 @@ export class WebGPURenderer {
155
176
  if (frame.width === 0 || frame.height === 0 || frame.items.length === 0)
156
177
  return;
157
178
  this.ensureDepth();
158
- this.ensureUniformCapacity(frame.items.length);
179
+ const draws = [];
180
+ for (const item of frame.items) {
181
+ if (!item.transparency)
182
+ draws.push({ item, opacity: 1, pipeline: this.pipeline });
183
+ }
184
+ for (const item of frame.items) {
185
+ const transparency = item.transparency;
186
+ if (!transparency)
187
+ continue;
188
+ if (transparency.mode === "two-sided") {
189
+ const resolved = resolveTwoSidedOpacity(transparency);
190
+ draws.push({
191
+ item,
192
+ opacity: resolved.back,
193
+ pipeline: this.transparentBackPipeline,
194
+ });
195
+ draws.push({
196
+ item,
197
+ opacity: resolved.front,
198
+ pipeline: this.transparentFrontPipeline,
199
+ });
200
+ }
201
+ else {
202
+ draws.push({
203
+ item,
204
+ opacity: opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY),
205
+ pipeline: this.transparentFrontPipeline,
206
+ });
207
+ }
208
+ }
209
+ this.ensureUniformCapacity(draws.length);
159
210
  const viewProj = multiply(CLIP_Z_FIX, frame.viewProjection);
160
211
  const layout = this.pipeline.getBindGroupLayout(0);
161
212
  const bindGroup = this.device.createBindGroup({
@@ -164,12 +215,12 @@ export class WebGPURenderer {
164
215
  { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } },
165
216
  ],
166
217
  });
167
- frame.items.forEach((item, i) => {
218
+ draws.forEach((draw, i) => {
168
219
  const data = new Float32Array(UNIFORM_STRIDE / 4);
169
220
  data.set(viewProj, 0);
170
- data.set(item.model, 16);
221
+ data.set(draw.item.model, 16);
171
222
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
172
- data.set([frame.light.intensity, frame.light.ambient, 0, 0], 36);
223
+ data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
173
224
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
174
225
  });
175
226
  const encoder = this.device.createCommandEncoder();
@@ -189,9 +240,9 @@ export class WebGPURenderer {
189
240
  depthStoreOp: "store",
190
241
  },
191
242
  });
192
- pass.setPipeline(this.pipeline);
193
- frame.items.forEach((item, i) => {
194
- const mesh = this.buffers(item.mesh);
243
+ draws.forEach((draw, i) => {
244
+ const mesh = this.buffers(draw.item.mesh);
245
+ pass.setPipeline(draw.pipeline);
195
246
  pass.setBindGroup(0, bindGroup, [i * UNIFORM_STRIDE]);
196
247
  pass.setVertexBuffer(0, mesh.position);
197
248
  pass.setVertexBuffer(1, mesh.normal);
@@ -215,6 +266,8 @@ export class WebGPURenderer {
215
266
  this.device = undefined;
216
267
  this.context = undefined;
217
268
  this.pipeline = undefined;
269
+ this.transparentBackPipeline = undefined;
270
+ this.transparentFrontPipeline = undefined;
218
271
  this.uniformBuffer = undefined;
219
272
  this.depthTexture = undefined;
220
273
  this.canvas = undefined;