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,129 @@
1
+ import {
2
+ SHADER_BIND_GROUP,
3
+ SHADER_BINDING,
4
+ SHADOW_BINDING,
5
+ } from '../../core/pipelineConstants.js';
6
+ import { VERTEX_ATTRIBUTE } from './vertexLayout.js';
7
+ import { MAX_POINT_LIGHTS } from '../constants.js';
8
+
9
+ export { MAX_POINT_LIGHTS };
10
+
11
+ /** Object-uniform layout shared by color and shadow-depth shader modules. */
12
+ export const OBJECT_UNIFORM_WGSL = /* wgsl */ `
13
+ struct ObjectUniforms {
14
+ model: mat4x4f,
15
+ normalMatrix: mat4x4f,
16
+ color: vec4f,
17
+ // x: whether this mesh receives the directional shadow.
18
+ shadowFlags: vec4f,
19
+ };
20
+ `;
21
+
22
+ /** Directional-shadow uniform layout shared by both render passes. */
23
+ export const SHADOW_UNIFORM_WGSL = /* wgsl */ `
24
+ struct ShadowUniforms {
25
+ viewProjection: mat4x4f,
26
+ // x: enabled, y: depth bias, z: normal bias, w: shadow texel size.
27
+ params: vec4f,
28
+ };
29
+ `;
30
+
31
+ /** Uniform declarations and shading helpers shared by every 3D shader. */
32
+ export const SHARED_SHADER_CHUNKS = /* wgsl */ `
33
+ struct PointLightUniform {
34
+ position: vec3f, // world space
35
+ color: vec3f, // linear, premultiplied by intensity
36
+ };
37
+
38
+ struct FrameUniforms {
39
+ viewProjection: mat4x4f,
40
+ lightDirection: vec3f,
41
+ lightColor: vec3f,
42
+ ambientColor: vec3f,
43
+ // Packs into ambientColor's 16-byte slot; how many array entries are live.
44
+ pointLightCount: f32,
45
+ pointLights: array<PointLightUniform, ${MAX_POINT_LIGHTS}>,
46
+ };
47
+
48
+ ${OBJECT_UNIFORM_WGSL}
49
+ ${SHADOW_UNIFORM_WGSL}
50
+
51
+ @group(${SHADER_BIND_GROUP.frame}) @binding(${SHADER_BINDING.uniforms})
52
+ var<uniform> uFrame: FrameUniforms;
53
+ @group(${SHADER_BIND_GROUP.object}) @binding(${SHADER_BINDING.uniforms})
54
+ var<uniform> uObject: ObjectUniforms;
55
+ @group(${SHADER_BIND_GROUP.shadow}) @binding(${SHADOW_BINDING.uniforms})
56
+ var<uniform> uShadow: ShadowUniforms;
57
+ @group(${SHADER_BIND_GROUP.shadow}) @binding(${SHADOW_BINDING.map})
58
+ var uShadowMap: texture_depth_2d;
59
+ @group(${SHADER_BIND_GROUP.shadow}) @binding(${SHADOW_BINDING.sampler})
60
+ var uShadowSampler: sampler_comparison;
61
+
62
+ struct VertexIn {
63
+ @location(${VERTEX_ATTRIBUTE.position}) position: vec3f,
64
+ @location(${VERTEX_ATTRIBUTE.normal}) normal: vec3f,
65
+ @location(${VERTEX_ATTRIBUTE.uv}) uv: vec2f,
66
+ };
67
+
68
+ // Returns the fraction of the 3x3 comparison-filter kernel that can see the
69
+ // directional light. Coordinates outside the configured shadow camera stay
70
+ // fully lit rather than clamping to the map's edge.
71
+ fn directionalShadow(n: vec3f, worldPosition: vec3f) -> f32 {
72
+ if (uShadow.params.x < 0.5 || uObject.shadowFlags.x < 0.5) {
73
+ return 1.0;
74
+ }
75
+
76
+ let biasedPosition = worldPosition + n * uShadow.params.z;
77
+ let clip = uShadow.viewProjection * vec4f(biasedPosition, 1.0);
78
+ if (clip.w <= 0.0) {
79
+ return 1.0;
80
+ }
81
+
82
+ let ndc = clip.xyz / clip.w;
83
+ let uv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);
84
+ if (
85
+ uv.x < 0.0 || uv.x > 1.0 ||
86
+ uv.y < 0.0 || uv.y > 1.0 ||
87
+ ndc.z < 0.0 || ndc.z > 1.0
88
+ ) {
89
+ return 1.0;
90
+ }
91
+
92
+ let referenceDepth = ndc.z - uShadow.params.y;
93
+ var visibility = 0.0;
94
+ for (var y = -1; y <= 1; y++) {
95
+ for (var x = -1; x <= 1; x++) {
96
+ let offset = vec2f(f32(x), f32(y)) * uShadow.params.w;
97
+ visibility += textureSampleCompareLevel(
98
+ uShadowMap,
99
+ uShadowSampler,
100
+ uv + offset,
101
+ referenceDepth,
102
+ );
103
+ }
104
+ }
105
+ return visibility / 9.0;
106
+ }
107
+
108
+ // Ambient + the directional light + the point lights on a diffuse
109
+ // surface, in linear space. Lit fragment shaders multiply the surface
110
+ // color by this.
111
+ fn diffuseLighting(n: vec3f, worldPosition: vec3f) -> vec3f {
112
+ var lighting = uFrame.ambientColor;
113
+ let toLight = normalize(-uFrame.lightDirection);
114
+ let shadow = directionalShadow(n, worldPosition);
115
+ lighting += max(dot(n, toLight), 0.0) * uFrame.lightColor * shadow;
116
+ let count = u32(uFrame.pointLightCount);
117
+ for (var i = 0u; i < count; i++) {
118
+ let offset = uFrame.pointLights[i].position - worldPosition;
119
+ let distanceSq = max(dot(offset, offset), 1e-6);
120
+ // 1 / (1 + d^2) falloff: intensity is the brightness right at the
121
+ // light, fading smoothly and never blowing up at zero distance.
122
+ let attenuation = 1.0 / (1.0 + distanceSq);
123
+ let direction = offset * inverseSqrt(distanceSq);
124
+ lighting += max(dot(n, direction), 0.0) * attenuation
125
+ * uFrame.pointLights[i].color;
126
+ }
127
+ return lighting;
128
+ }
129
+ `;
@@ -0,0 +1,85 @@
1
+ import { VERTEX_STRIDE } from '../geometries/Geometry.js';
2
+ import { INSTANCE_SIZE } from '../constants.js';
3
+ import { INSTANCE_STEP_MODE } from '../../core/pipelineConstants.js';
4
+
5
+ const FLOAT_BYTES = Float32Array.BYTES_PER_ELEMENT;
6
+ const VEC2_BYTES = 2 * FLOAT_BYTES;
7
+ const VEC3_BYTES = 3 * FLOAT_BYTES;
8
+ const VEC4_BYTES = 4 * FLOAT_BYTES;
9
+ const MAT4_BYTES = 4 * VEC4_BYTES;
10
+
11
+ /** Vertex input locations shared by WGSL and the GPU buffer descriptors. */
12
+ export const VERTEX_ATTRIBUTE = Object.freeze({
13
+ position: 0,
14
+ normal: 1,
15
+ uv: 2,
16
+ instanceMatrix0: 3,
17
+ instanceMatrix1: 4,
18
+ instanceMatrix2: 5,
19
+ instanceMatrix3: 6,
20
+ instanceColor: 7,
21
+ });
22
+
23
+ export const GEOMETRY_VERTEX_BUFFER_LAYOUT = Object.freeze({
24
+ arrayStride: VERTEX_STRIDE,
25
+ attributes: Object.freeze([
26
+ Object.freeze({
27
+ shaderLocation: VERTEX_ATTRIBUTE.position,
28
+ offset: 0,
29
+ format: 'float32x3',
30
+ }),
31
+ Object.freeze({
32
+ shaderLocation: VERTEX_ATTRIBUTE.normal,
33
+ offset: VEC3_BYTES,
34
+ format: 'float32x3',
35
+ }),
36
+ Object.freeze({
37
+ shaderLocation: VERTEX_ATTRIBUTE.uv,
38
+ offset: 2 * VEC3_BYTES,
39
+ format: 'float32x2',
40
+ }),
41
+ ]),
42
+ });
43
+
44
+ export const INSTANCE_VERTEX_BUFFER_LAYOUT = Object.freeze({
45
+ arrayStride: INSTANCE_SIZE * FLOAT_BYTES,
46
+ stepMode: INSTANCE_STEP_MODE,
47
+ attributes: Object.freeze([
48
+ Object.freeze({
49
+ shaderLocation: VERTEX_ATTRIBUTE.instanceMatrix0,
50
+ offset: 0,
51
+ format: 'float32x4',
52
+ }),
53
+ Object.freeze({
54
+ shaderLocation: VERTEX_ATTRIBUTE.instanceMatrix1,
55
+ offset: VEC4_BYTES,
56
+ format: 'float32x4',
57
+ }),
58
+ Object.freeze({
59
+ shaderLocation: VERTEX_ATTRIBUTE.instanceMatrix2,
60
+ offset: 2 * VEC4_BYTES,
61
+ format: 'float32x4',
62
+ }),
63
+ Object.freeze({
64
+ shaderLocation: VERTEX_ATTRIBUTE.instanceMatrix3,
65
+ offset: 3 * VEC4_BYTES,
66
+ format: 'float32x4',
67
+ }),
68
+ Object.freeze({
69
+ shaderLocation: VERTEX_ATTRIBUTE.instanceColor,
70
+ offset: MAT4_BYTES,
71
+ format: 'float32x4',
72
+ }),
73
+ ]),
74
+ });
75
+
76
+ const STANDARD_VERTEX_BUFFERS = Object.freeze([GEOMETRY_VERTEX_BUFFER_LAYOUT]);
77
+ const INSTANCED_VERTEX_BUFFERS = Object.freeze([
78
+ GEOMETRY_VERTEX_BUFFER_LAYOUT,
79
+ INSTANCE_VERTEX_BUFFER_LAYOUT,
80
+ ]);
81
+
82
+ /** GPU vertex buffer layouts for a regular or instanced mesh pipeline. */
83
+ export function vertexBufferLayouts(instanced) {
84
+ return instanced ? INSTANCED_VERTEX_BUFFERS : STANDARD_VERTEX_BUFFERS;
85
+ }
@@ -0,0 +1,70 @@
1
+ import { SHARED_SHADER_CHUNKS } from './shared.js';
2
+ import { VERTEX_ATTRIBUTE } from './vertexLayout.js';
3
+
4
+ /** Shared chunks plus the regular mesh vertex stage. Append a fragment stage. */
5
+ export const MESH_SHADER_PREFIX =
6
+ SHARED_SHADER_CHUNKS +
7
+ /* wgsl */ `
8
+ struct VertexOut {
9
+ @builtin(position) position: vec4f,
10
+ @location(0) worldNormal: vec3f,
11
+ @location(1) uv: vec2f,
12
+ @location(2) worldPosition: vec3f,
13
+ };
14
+
15
+ @vertex
16
+ fn vs(input: VertexIn) -> VertexOut {
17
+ var out: VertexOut;
18
+ let worldPosition = uObject.model * vec4f(input.position, 1.0);
19
+ out.position = uFrame.viewProjection * worldPosition;
20
+ out.worldNormal = (uObject.normalMatrix * vec4f(input.normal, 0.0)).xyz;
21
+ out.uv = input.uv;
22
+ out.worldPosition = worldPosition.xyz;
23
+ return out;
24
+ }
25
+
26
+ fn objectColor(input: VertexOut) -> vec4f {
27
+ return uObject.color;
28
+ }
29
+ `;
30
+
31
+ /** Shared chunks plus the per-instance mesh vertex stage. Append a fragment stage. */
32
+ export const INSTANCED_MESH_SHADER_PREFIX =
33
+ SHARED_SHADER_CHUNKS +
34
+ /* wgsl */ `
35
+ struct InstanceIn {
36
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix0}) im0: vec4f,
37
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix1}) im1: vec4f,
38
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix2}) im2: vec4f,
39
+ @location(${VERTEX_ATTRIBUTE.instanceMatrix3}) im3: vec4f,
40
+ @location(${VERTEX_ATTRIBUTE.instanceColor}) color: vec4f,
41
+ };
42
+
43
+ struct VertexOut {
44
+ @builtin(position) position: vec4f,
45
+ @location(0) worldNormal: vec3f,
46
+ @location(1) uv: vec2f,
47
+ @location(2) color: vec4f,
48
+ @location(3) worldPosition: vec3f,
49
+ };
50
+
51
+ @vertex
52
+ fn vs(input: VertexIn, instance: InstanceIn) -> VertexOut {
53
+ var out: VertexOut;
54
+ let instanceMatrix = mat4x4f(instance.im0, instance.im1, instance.im2, instance.im3);
55
+ let worldPosition = uObject.model * instanceMatrix * vec4f(input.position, 1.0);
56
+ out.position = uFrame.viewProjection * worldPosition;
57
+ // The instance matrix's upper 3x3 handles rotation + uniform scale
58
+ // (lighting normalizes); per-instance non-uniform scale skews normals.
59
+ let rotation = mat3x3f(instance.im0.xyz, instance.im1.xyz, instance.im2.xyz);
60
+ out.worldNormal = (uObject.normalMatrix * vec4f(rotation * input.normal, 0.0)).xyz;
61
+ out.uv = input.uv;
62
+ out.color = instance.color;
63
+ out.worldPosition = worldPosition.xyz;
64
+ return out;
65
+ }
66
+
67
+ fn objectColor(input: VertexOut) -> vec4f {
68
+ return input.color * uObject.color;
69
+ }
70
+ `;
@@ -0,0 +1,258 @@
1
+ export const TOUCH_GESTURE = Object.freeze({
2
+ PAN: 'pan',
3
+ ROTATE: 'rotate',
4
+ });
5
+
6
+ const MOUSE_POINTER_TYPE = 'mouse';
7
+ const RIGHT_MOUSE_BUTTON = 2;
8
+ const LEFT_MOUSE_BUTTON = 0;
9
+ const TWO_TOUCHES = 2;
10
+ const NDC_SPAN = 2;
11
+ const CONTEXT_MENU_PAN_THRESHOLD = 3;
12
+ const DEFAULT_ROTATE_SPEED = 0.005;
13
+ const DEFAULT_ZOOM_SPEED = 0.001;
14
+ const touchActionClaims = new WeakMap();
15
+
16
+ function claimTouchAction(element) {
17
+ let claim = touchActionClaims.get(element);
18
+ if (!claim) {
19
+ claim = { owners: 0, previous: element.style.touchAction };
20
+ touchActionClaims.set(element, claim);
21
+ }
22
+ claim.owners++;
23
+ element.style.touchAction = 'none';
24
+ }
25
+
26
+ function releaseTouchAction(element) {
27
+ const claim = touchActionClaims.get(element);
28
+ if (!claim) return;
29
+ claim.owners--;
30
+ if (claim.owners === 0) {
31
+ element.style.touchAction = claim.previous;
32
+ touchActionClaims.delete(element);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * The pointer / wheel plumbing shared by the camera controls
38
+ * (OrbitControls in 3D, PanZoomControls in 2D). Owns everything about
39
+ * reading the input device: button roles, pointer capture, keeping the
40
+ * browser context menu available after a plain right-click, turning
41
+ * wheel events into zoom factors with the cursor position in NDC, and
42
+ * the touch gestures.
43
+ *
44
+ * Mouse gestures: right-drag pans, Alt + left-drag rotates, scroll
45
+ * zooms toward the cursor. Touch gestures: a one-finger drag pans or
46
+ * rotates (see `singleTouchGesture`), a two-finger drag pans, and
47
+ * pinching zooms toward the midpoint between the fingers.
48
+ *
49
+ * Subclasses implement just the camera math:
50
+ * _pan(dx, dy) pan deltas, in pixels
51
+ * _rotate(rx, ry) rotation deltas, pre-scaled by rotateSpeed
52
+ * _zoom(factor, ndcX, ndcY) zoom factor plus the cursor in -1..1 NDC
53
+ */
54
+ export class PointerControls {
55
+ constructor(domElement) {
56
+ this.domElement = domElement;
57
+
58
+ /** Set to false to ignore input, e.g. while dragging an object. */
59
+ this.enabled = true;
60
+ this.rotateSpeed = DEFAULT_ROTATE_SPEED;
61
+ this.zoomSpeed = DEFAULT_ZOOM_SPEED;
62
+ /** Set to false to disable the rotate gesture (pan and zoom still work). */
63
+ this.enableRotate = true;
64
+ /**
65
+ * What a one-finger touch drag does: 'pan' (default) or 'rotate'.
66
+ * Two-finger gestures always pan and pinch-zoom.
67
+ */
68
+ this.singleTouchGesture = TOUCH_GESTURE.PAN;
69
+
70
+ this._dragging = false;
71
+ this._panning = false;
72
+ this._leftDrag = false;
73
+ this._panDistance = 0;
74
+ this._lastX = 0;
75
+ this._lastY = 0;
76
+ this._mousePointerId = null;
77
+ this._touches = new Map(); // pointerId -> {x, y} of active touch points
78
+ this._pinchDistance = 0;
79
+ this._disposed = false;
80
+
81
+ // Without this the browser claims touch drags for scrolling and
82
+ // pinches for page zoom before the pointer events reach us.
83
+ claimTouchAction(domElement);
84
+
85
+ this._onPointerDown = this._handlePointerDown.bind(this);
86
+ this._onPointerMove = this._handlePointerMove.bind(this);
87
+ this._onPointerUp = this._handlePointerUp.bind(this);
88
+ this._onContextMenu = this._handleContextMenu.bind(this);
89
+ this._onWheel = this._handleWheel.bind(this);
90
+
91
+ domElement.addEventListener('pointerdown', this._onPointerDown);
92
+ domElement.addEventListener('pointermove', this._onPointerMove);
93
+ domElement.addEventListener('pointerup', this._onPointerUp);
94
+ domElement.addEventListener('pointercancel', this._onPointerUp);
95
+ domElement.addEventListener('contextmenu', this._onContextMenu);
96
+ domElement.addEventListener('wheel', this._onWheel, { passive: false });
97
+ }
98
+
99
+ _handlePointerDown(event) {
100
+ if (!this.enabled) return;
101
+ if (event.pointerType !== MOUSE_POINTER_TYPE) {
102
+ this._touches.set(event.pointerId, {
103
+ x: event.clientX,
104
+ y: event.clientY,
105
+ });
106
+ if (this._touches.size === TWO_TOUCHES) {
107
+ const [first, second] = this._touches.values();
108
+ this._pinchDistance = Math.hypot(
109
+ first.x - second.x,
110
+ first.y - second.y,
111
+ );
112
+ }
113
+ this.domElement.setPointerCapture(event.pointerId);
114
+ return;
115
+ }
116
+
117
+ this._dragging = true;
118
+ this._mousePointerId = event.pointerId;
119
+ this._panning = event.button === RIGHT_MOUSE_BUTTON;
120
+ this._leftDrag = event.button === LEFT_MOUSE_BUTTON;
121
+ if (this._panning) this._panDistance = 0;
122
+ this._lastX = event.clientX;
123
+ this._lastY = event.clientY;
124
+ this.domElement.setPointerCapture(event.pointerId);
125
+ }
126
+
127
+ _handlePointerMove(event) {
128
+ if (!this.enabled) return;
129
+ if (event.pointerType !== MOUSE_POINTER_TYPE) {
130
+ this._touchMove(event);
131
+ return;
132
+ }
133
+ if (!this._dragging || event.pointerId !== this._mousePointerId) return;
134
+
135
+ const dx = event.clientX - this._lastX;
136
+ const dy = event.clientY - this._lastY;
137
+ this._lastX = event.clientX;
138
+ this._lastY = event.clientY;
139
+ if (this._panning) {
140
+ this._panDistance += Math.abs(dx) + Math.abs(dy);
141
+ this._pan(dx, dy);
142
+ } else if (this._leftDrag && event.altKey && this.enableRotate) {
143
+ // Alt is checked per move event, so pressing it mid-drag starts
144
+ // rotating and releasing it pauses.
145
+ this._rotate(dx * this.rotateSpeed, dy * this.rotateSpeed);
146
+ }
147
+ }
148
+
149
+ _handlePointerUp(event) {
150
+ if (event.pointerType !== MOUSE_POINTER_TYPE) {
151
+ this._touches.delete(event.pointerId);
152
+ // Dropping below two fingers re-arms the pinch for next time.
153
+ this._pinchDistance = 0;
154
+ } else {
155
+ if (event.pointerId !== this._mousePointerId) return;
156
+ this._dragging = false;
157
+ this._mousePointerId = null;
158
+ }
159
+ this._releasePointer(event.pointerId);
160
+ }
161
+
162
+ _handleContextMenu(event) {
163
+ if (!this.enabled) return;
164
+ const wasPanGesture = this._panDistance > CONTEXT_MENU_PAN_THRESHOLD;
165
+ if (wasPanGesture || this._touches.size > 0) event.preventDefault();
166
+ }
167
+
168
+ _handleWheel(event) {
169
+ if (!this.enabled) return;
170
+ event.preventDefault();
171
+ const factor = Math.exp(event.deltaY * this.zoomSpeed);
172
+ const ndc = this._toNdc(event.clientX, event.clientY);
173
+ this._zoom(factor, ndc.x, ndc.y);
174
+ }
175
+
176
+ _releasePointer(pointerId) {
177
+ if (this.domElement.hasPointerCapture(pointerId)) {
178
+ this.domElement.releasePointerCapture(pointerId);
179
+ }
180
+ }
181
+
182
+ /** A client-space position in normalized device coordinates (-1..1). */
183
+ _toNdc(clientX, clientY) {
184
+ const rect = this.domElement.getBoundingClientRect();
185
+ return {
186
+ x: ((clientX - rect.left) / rect.width) * NDC_SPAN - 1,
187
+ y: 1 - ((clientY - rect.top) / rect.height) * NDC_SPAN,
188
+ };
189
+ }
190
+
191
+ _touchMove(e) {
192
+ const touch = this._touches.get(e.pointerId);
193
+ if (!touch) return;
194
+
195
+ if (this._touches.size === 1) {
196
+ const dx = e.clientX - touch.x;
197
+ const dy = e.clientY - touch.y;
198
+ touch.x = e.clientX;
199
+ touch.y = e.clientY;
200
+ if (
201
+ this.singleTouchGesture === TOUCH_GESTURE.ROTATE &&
202
+ this.enableRotate
203
+ ) {
204
+ this._rotate(dx * this.rotateSpeed, dy * this.rotateSpeed);
205
+ } else {
206
+ this._pan(dx, dy);
207
+ }
208
+ return;
209
+ }
210
+
211
+ // Two fingers: pan by the midpoint's motion and zoom by the pinch.
212
+ // Any finger beyond the first two is tracked but ignored.
213
+ const [a, b] = this._touches.values();
214
+ const midXBefore = (a.x + b.x) / 2;
215
+ const midYBefore = (a.y + b.y) / 2;
216
+ touch.x = e.clientX;
217
+ touch.y = e.clientY;
218
+ const midX = (a.x + b.x) / 2;
219
+ const midY = (a.y + b.y) / 2;
220
+ this._pan(midX - midXBefore, midY - midYBefore);
221
+
222
+ const distance = Math.hypot(a.x - b.x, a.y - b.y);
223
+ if (this._pinchDistance > 0 && distance > 0) {
224
+ // Spreading shrinks the factor below 1 (zoom in), matching the
225
+ // wheel's Math.exp convention.
226
+ const ndc = this._toNdc(midX, midY);
227
+ this._zoom(this._pinchDistance / distance, ndc.x, ndc.y);
228
+ }
229
+ this._pinchDistance = distance;
230
+ }
231
+
232
+ /* The camera math, provided by subclasses. */
233
+ _pan(dx, dy) {}
234
+ _rotate(rx, ry) {}
235
+ _zoom(factor, ndcX, ndcY) {}
236
+
237
+ dispose() {
238
+ if (this._disposed) return;
239
+ this._disposed = true;
240
+ if (this._mousePointerId !== null) {
241
+ this._releasePointer(this._mousePointerId);
242
+ this._mousePointerId = null;
243
+ }
244
+ for (const pointerId of this._touches.keys()) {
245
+ this._releasePointer(pointerId);
246
+ }
247
+ this._touches.clear();
248
+ this._dragging = false;
249
+ this._pinchDistance = 0;
250
+ releaseTouchAction(this.domElement);
251
+ this.domElement.removeEventListener('pointerdown', this._onPointerDown);
252
+ this.domElement.removeEventListener('pointermove', this._onPointerMove);
253
+ this.domElement.removeEventListener('pointerup', this._onPointerUp);
254
+ this.domElement.removeEventListener('pointercancel', this._onPointerUp);
255
+ this.domElement.removeEventListener('contextmenu', this._onContextMenu);
256
+ this.domElement.removeEventListener('wheel', this._onWheel);
257
+ }
258
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * An image a shader can sample, plus its sampler settings — plain data,
3
+ * like geometries and materials. Assign one to a material's `map` and
4
+ * the renderer uploads it lazily on first draw (see GpuResources /
5
+ * GpuResources2d, which cache the GPU side per device on `_gpu`).
6
+ *
7
+ * `source` is anything copyExternalImageToTexture accepts: an
8
+ * ImageBitmap, HTMLCanvasElement, OffscreenCanvas or HTMLImageElement.
9
+ * Use `Texture.load(url)` to get one from an image file.
10
+ *
11
+ * One Texture can be shared by renderers on the same or different
12
+ * devices. Each device receives its own GPU texture and sampler.
13
+ */
14
+ export class Texture {
15
+ /**
16
+ * @param {ImageBitmap|HTMLCanvasElement|OffscreenCanvas|HTMLImageElement} source
17
+ * @param {object} [options]
18
+ * @param {GPUFilterMode} [options.magFilter] 'linear' (default) or 'nearest'
19
+ * @param {GPUFilterMode} [options.minFilter] 'linear' (default) or 'nearest'
20
+ * @param {GPUAddressMode} [options.addressModeU] 'clamp-to-edge' (default),
21
+ * 'repeat' or 'mirror-repeat' — what happens outside 0..1 UVs
22
+ * @param {GPUAddressMode} [options.addressModeV] same, for v
23
+ * @param {boolean} [options.flipY] flip the image vertically on upload;
24
+ * useful when a geometry's v axis points up (e.g. RectGeometry) so
25
+ * images would otherwise appear upside down
26
+ * @param {boolean} [options.mipmaps] generate a full mip chain on
27
+ * upload (default true) so minified sampling doesn't alias; turn
28
+ * off for textures that never shrink on screen (UI, full-screen)
29
+ * @param {boolean} [options.srgb] the image holds sRGB-encoded colors
30
+ * (default true — the normal case for pictures): the GPU decodes
31
+ * samples to linear for shading, and mipmaps are filtered in linear
32
+ * space. Turn off for data textures whose values aren't colors
33
+ */
34
+ constructor(
35
+ source,
36
+ {
37
+ magFilter = 'linear',
38
+ minFilter = 'linear',
39
+ addressModeU = 'clamp-to-edge',
40
+ addressModeV = 'clamp-to-edge',
41
+ flipY = false,
42
+ mipmaps = true,
43
+ srgb = true,
44
+ } = {},
45
+ ) {
46
+ this.source = source;
47
+ this.magFilter = magFilter;
48
+ this.minFilter = minFilter;
49
+ this.addressModeU = addressModeU;
50
+ this.addressModeV = addressModeV;
51
+ this.flipY = flipY;
52
+ this.mipmaps = mipmaps;
53
+ this.srgb = srgb;
54
+ this._gpu = null;
55
+ }
56
+
57
+ get width() {
58
+ return this.source.width;
59
+ }
60
+
61
+ get height() {
62
+ return this.source.height;
63
+ }
64
+
65
+ /**
66
+ * Destroys every uploaded GPU texture (if any), releasing its memory
67
+ * right away instead of waiting for GC. The next draw on each device
68
+ * uploads it again, and meshes/shapes rebuild their bind groups.
69
+ */
70
+ dispose() {
71
+ if (this._gpu) {
72
+ for (const { texture } of this._gpu.values()) texture.destroy();
73
+ this._gpu = null;
74
+ }
75
+ return this;
76
+ }
77
+
78
+ /** Fetches an image file and wraps it in a Texture. */
79
+ static async load(url, options) {
80
+ const response = await fetch(url);
81
+ if (!response.ok) {
82
+ throw new Error(`Texture.load: ${url} responded ${response.status}`);
83
+ }
84
+ const bitmap = await createImageBitmap(await response.blob());
85
+ return new Texture(bitmap, options);
86
+ }
87
+ }