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.
- package/README.md +556 -0
- package/package.json +37 -0
- package/src/2d/cameras/Camera2d.js +46 -0
- package/src/2d/constants.js +3 -0
- package/src/2d/controls/DragControls2d.js +174 -0
- package/src/2d/controls/PanZoomControls.js +64 -0
- package/src/2d/core/GpuResources2d.js +163 -0
- package/src/2d/core/InstancedShape2d.js +74 -0
- package/src/2d/core/Object2d.js +107 -0
- package/src/2d/core/Pipelines2d.js +110 -0
- package/src/2d/core/Renderer2d.js +353 -0
- package/src/2d/core/Scene2d.js +13 -0
- package/src/2d/core/Shape2d.js +13 -0
- package/src/2d/core/Uniforms2d.js +13 -0
- package/src/2d/geometries/CircleGeometry.js +27 -0
- package/src/2d/geometries/Geometry2d.js +108 -0
- package/src/2d/geometries/RectGeometry.js +23 -0
- package/src/2d/materials/BasicMaterial2d.js +12 -0
- package/src/2d/materials/Material2d.js +78 -0
- package/src/2d/materials/SpriteMaterial2d.js +28 -0
- package/src/2d/shaders/fragments.js +23 -0
- package/src/2d/shaders/shared.js +28 -0
- package/src/2d/shaders/vertexLayout.js +74 -0
- package/src/2d/shaders/vertexStages.js +59 -0
- package/src/3d/cameras/Camera.js +66 -0
- package/src/3d/cameras/OrthographicCamera.js +35 -0
- package/src/3d/cameras/PerspectiveCamera.js +25 -0
- package/src/3d/constants.js +18 -0
- package/src/3d/controls/DragControls.js +168 -0
- package/src/3d/controls/OrbitControls.js +125 -0
- package/src/3d/core/DirectionalShadowMap.js +283 -0
- package/src/3d/core/GpuResources.js +174 -0
- package/src/3d/core/InstanceMatrix.js +55 -0
- package/src/3d/core/InstancedBounds.js +114 -0
- package/src/3d/core/InstancedMesh.js +124 -0
- package/src/3d/core/Mesh.js +26 -0
- package/src/3d/core/Object3d.js +101 -0
- package/src/3d/core/Pipelines.js +125 -0
- package/src/3d/core/RayIntersection.js +184 -0
- package/src/3d/core/Raycaster.js +246 -0
- package/src/3d/core/Renderer.js +492 -0
- package/src/3d/core/Scene.js +13 -0
- package/src/3d/core/ShadowPipelines.js +64 -0
- package/src/3d/core/Uniforms.js +132 -0
- package/src/3d/geometries/BoxGeometry.js +56 -0
- package/src/3d/geometries/Geometry.js +97 -0
- package/src/3d/geometries/PlaneGeometry.js +24 -0
- package/src/3d/geometries/SphereGeometry.js +46 -0
- package/src/3d/geometries/WireframeGeometry.js +39 -0
- package/src/3d/helpers/GridHelper.js +43 -0
- package/src/3d/lights/AmbientLight.js +18 -0
- package/src/3d/lights/DirectionalLight.js +26 -0
- package/src/3d/lights/DirectionalShadow.js +29 -0
- package/src/3d/lights/PointLight.js +23 -0
- package/src/3d/materials/BasicMaterial.js +11 -0
- package/src/3d/materials/LambertMaterial.js +13 -0
- package/src/3d/materials/Material.js +87 -0
- package/src/3d/materials/TextureMaterial.js +24 -0
- package/src/3d/shaders/fragments.js +34 -0
- package/src/3d/shaders/shadows.js +52 -0
- package/src/3d/shaders/shared.js +129 -0
- package/src/3d/shaders/vertexLayout.js +85 -0
- package/src/3d/shaders/vertexStages.js +70 -0
- package/src/core/PointerControls.js +258 -0
- package/src/core/Texture.js +87 -0
- package/src/core/createMaterialPipelineLayouts.js +114 -0
- package/src/core/deviceLease.js +67 -0
- package/src/core/generateMipmaps.js +115 -0
- package/src/core/indexedTriangles.js +53 -0
- package/src/core/initWebGpu.js +65 -0
- package/src/core/materialResources.js +18 -0
- package/src/core/objectGpuResources.js +68 -0
- package/src/core/pipelineConstants.js +68 -0
- package/src/core/rendererConfig.js +66 -0
- package/src/core/uploadTexture.js +57 -0
- package/src/index.js +68 -0
- package/src/math/Frustum.js +143 -0
- package/src/math/Mat3.js +165 -0
- package/src/math/Mat4.js +359 -0
- package/src/math/Vec2.js +72 -0
- package/src/math/Vec3.js +98 -0
- package/src/math/color.js +20 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Mesh } from './Mesh.js';
|
|
2
|
+
import { srgbToLinear } from '../../math/color.js';
|
|
3
|
+
import {
|
|
4
|
+
INSTANCE_COLOR_OFFSET,
|
|
5
|
+
INSTANCE_SIZE,
|
|
6
|
+
} from '../constants.js';
|
|
7
|
+
import { computeInstancedBounds } from './InstancedBounds.js';
|
|
8
|
+
|
|
9
|
+
export { INSTANCE_SIZE } from '../constants.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A Mesh drawn `count` times in one draw call. Each instance has its
|
|
13
|
+
* own transform and color, packed into `instanceData` and uploaded as
|
|
14
|
+
* an instance-step vertex buffer — thousands of copies cost one
|
|
15
|
+
* uniform upload and one drawIndexed instead of thousands.
|
|
16
|
+
*
|
|
17
|
+
* Instance transforms are local to the mesh: the mesh's own scene-graph
|
|
18
|
+
* transform applies on top, and instance colors multiply with the
|
|
19
|
+
* material's color. Write instances with `setMatrixAt`/`setColorAt` —
|
|
20
|
+
* they mark the buffer for re-upload; set `needsUpdate = true` yourself
|
|
21
|
+
* only if you write `instanceData` directly.
|
|
22
|
+
*
|
|
23
|
+
* The instance count is fixed at construction. To show fewer instances,
|
|
24
|
+
* scale the extras to zero; to grow, make a new InstancedMesh.
|
|
25
|
+
*
|
|
26
|
+
* Raycaster tests every instance transform and includes `instanceId` in
|
|
27
|
+
* instanced hits. DragControls still moves the InstancedMesh as one batch.
|
|
28
|
+
*/
|
|
29
|
+
export class InstancedMesh extends Mesh {
|
|
30
|
+
constructor(geometry, material, count) {
|
|
31
|
+
super(geometry, material);
|
|
32
|
+
this.isInstanced = true;
|
|
33
|
+
this.count = count;
|
|
34
|
+
this.instanceData = new Float32Array(count * INSTANCE_SIZE);
|
|
35
|
+
// Every instance starts as an identity matrix with a white color.
|
|
36
|
+
for (let i = 0; i < count; i++) {
|
|
37
|
+
const base = i * INSTANCE_SIZE;
|
|
38
|
+
this.instanceData[base] = 1;
|
|
39
|
+
this.instanceData[base + 5] = 1;
|
|
40
|
+
this.instanceData[base + 10] = 1;
|
|
41
|
+
this.instanceData[base + 15] = 1;
|
|
42
|
+
this.instanceData.fill(
|
|
43
|
+
1,
|
|
44
|
+
base + INSTANCE_COLOR_OFFSET,
|
|
45
|
+
base + INSTANCE_SIZE,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
// Resource caches compare revisions so every renderer/device receives
|
|
49
|
+
// an edit, even after another renderer has cleared needsUpdate.
|
|
50
|
+
this._instanceRevision = 0;
|
|
51
|
+
this._boundsRevision = 0;
|
|
52
|
+
this._needsUpdate = true;
|
|
53
|
+
this._instanceBounds = null;
|
|
54
|
+
this._instanceBoundsSource = null;
|
|
55
|
+
this._instanceBoundsRevision = -1;
|
|
56
|
+
this._instanceBoundsCount = -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mesh-local union of every transformed instance. Frustum culling remains
|
|
61
|
+
* batch-level: if this box intersects, all `count` instances draw together.
|
|
62
|
+
*/
|
|
63
|
+
get bounds() {
|
|
64
|
+
const sourceBounds = this.geometry?.bounds || null;
|
|
65
|
+
if (
|
|
66
|
+
this._instanceBoundsSource !== sourceBounds ||
|
|
67
|
+
this._instanceBoundsRevision !== this._boundsRevision ||
|
|
68
|
+
this._instanceBoundsCount !== this.count
|
|
69
|
+
) {
|
|
70
|
+
this._instanceBounds = computeInstancedBounds(
|
|
71
|
+
sourceBounds,
|
|
72
|
+
this.instanceData,
|
|
73
|
+
this.count,
|
|
74
|
+
);
|
|
75
|
+
this._instanceBoundsSource = sourceBounds;
|
|
76
|
+
this._instanceBoundsRevision = this._boundsRevision;
|
|
77
|
+
this._instanceBoundsCount = this.count;
|
|
78
|
+
}
|
|
79
|
+
return this._instanceBounds;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Upload hint; set true after changing `instanceData` directly. */
|
|
83
|
+
get needsUpdate() {
|
|
84
|
+
return this._needsUpdate;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
set needsUpdate(value) {
|
|
88
|
+
this._needsUpdate = Boolean(value);
|
|
89
|
+
if (this._needsUpdate) {
|
|
90
|
+
this._instanceRevision++;
|
|
91
|
+
// Direct writes may have changed matrices, so conservatively invalidate.
|
|
92
|
+
this._boundsRevision++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Copies a Mat4 into instance `index`'s transform. */
|
|
97
|
+
setMatrixAt(index, matrix) {
|
|
98
|
+
this.instanceData.set(matrix.elements, index * INSTANCE_SIZE);
|
|
99
|
+
this._markInstanceDataUpdated(true);
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Sets instance `index`'s color from [r, g, b] or [r, g, b, a] —
|
|
105
|
+
* sRGB display values like material colors. They are stored
|
|
106
|
+
* linearized (shading happens in linear space), so write linear
|
|
107
|
+
* values if you fill `instanceData` directly instead.
|
|
108
|
+
*/
|
|
109
|
+
setColorAt(index, color) {
|
|
110
|
+
const base = index * INSTANCE_SIZE + INSTANCE_COLOR_OFFSET;
|
|
111
|
+
this.instanceData[base] = srgbToLinear(color[0]);
|
|
112
|
+
this.instanceData[base + 1] = srgbToLinear(color[1]);
|
|
113
|
+
this.instanceData[base + 2] = srgbToLinear(color[2]);
|
|
114
|
+
this.instanceData[base + 3] = color.length > 3 ? color[3] : 1;
|
|
115
|
+
this._markInstanceDataUpdated(false);
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_markInstanceDataUpdated(boundsChanged) {
|
|
120
|
+
this._needsUpdate = true;
|
|
121
|
+
this._instanceRevision++;
|
|
122
|
+
if (boundsChanged) this._boundsRevision++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Object3d } from './Object3d.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A renderable object: a Geometry (the shape) combined with a Material
|
|
5
|
+
* (how the surface is shaded).
|
|
6
|
+
*/
|
|
7
|
+
export class Mesh extends Object3d {
|
|
8
|
+
constructor(geometry, material) {
|
|
9
|
+
super();
|
|
10
|
+
this.geometry = geometry;
|
|
11
|
+
this.material = material;
|
|
12
|
+
/** Set false to bypass camera and shadow-camera frustum culling. */
|
|
13
|
+
this.frustumCulled = true;
|
|
14
|
+
/** Whether this mesh is drawn into an enabled directional shadow map. */
|
|
15
|
+
this.castShadow = false;
|
|
16
|
+
/** Whether lit materials on this mesh are darkened by that shadow map. */
|
|
17
|
+
this.receiveShadow = false;
|
|
18
|
+
/** View-space depth, written by the renderer's transparent sort. */
|
|
19
|
+
this._viewDepth = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Local-space bounds used for conservative renderer culling. */
|
|
23
|
+
get bounds() {
|
|
24
|
+
return this.geometry?.bounds || null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Vec3 } from '../../math/Vec3.js';
|
|
2
|
+
import { Mat4 } from '../../math/Mat4.js';
|
|
3
|
+
import { disposeObjectGpuResources } from '../../core/objectGpuResources.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base class for everything that lives in the scene graph.
|
|
7
|
+
* Holds a transform (position / rotation / scale) and a list of children,
|
|
8
|
+
* so objects can be parented to each other like in three.js.
|
|
9
|
+
*/
|
|
10
|
+
export class Object3d {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.position = new Vec3(0, 0, 0);
|
|
13
|
+
/** Euler angles in radians, applied in XYZ order. */
|
|
14
|
+
this.rotation = new Vec3(0, 0, 0);
|
|
15
|
+
this.scale = new Vec3(1, 1, 1);
|
|
16
|
+
|
|
17
|
+
this.parent = null;
|
|
18
|
+
this.children = [];
|
|
19
|
+
/** Invisible objects — and everything under them — are skipped by rendering and picking. */
|
|
20
|
+
this.visible = true;
|
|
21
|
+
|
|
22
|
+
this.localMatrix = new Mat4();
|
|
23
|
+
this.worldMatrix = new Mat4();
|
|
24
|
+
/** Renderer-local GPU resources, created lazily (see GpuResources). */
|
|
25
|
+
this._gpu = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
add(child) {
|
|
29
|
+
// Adding an object to itself or to one of its own descendants would
|
|
30
|
+
// create a cycle and hang traverse/updateWorldMatrix — refuse it.
|
|
31
|
+
for (let node = this; node; node = node.parent) {
|
|
32
|
+
if (node === child) return this;
|
|
33
|
+
}
|
|
34
|
+
if (child.parent) child.parent.remove(child);
|
|
35
|
+
child.parent = this;
|
|
36
|
+
this.children.push(child);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
remove(child) {
|
|
41
|
+
const index = this.children.indexOf(child);
|
|
42
|
+
if (index !== -1) {
|
|
43
|
+
child.parent = null;
|
|
44
|
+
this.children.splice(index, 1);
|
|
45
|
+
}
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Recomputes localMatrix and worldMatrix for this object and all descendants. */
|
|
50
|
+
updateWorldMatrix() {
|
|
51
|
+
this.localMatrix.compose(this.position, this.rotation, this.scale);
|
|
52
|
+
if (this.parent) {
|
|
53
|
+
this.worldMatrix.multiplyMatrices(
|
|
54
|
+
this.parent.worldMatrix,
|
|
55
|
+
this.localMatrix,
|
|
56
|
+
);
|
|
57
|
+
} else {
|
|
58
|
+
this.worldMatrix.copy(this.localMatrix);
|
|
59
|
+
}
|
|
60
|
+
for (const child of this.children) {
|
|
61
|
+
child.updateWorldMatrix();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Calls `callback(object)` for this object and every descendant. */
|
|
66
|
+
traverse(callback) {
|
|
67
|
+
callback(this);
|
|
68
|
+
for (const child of this.children) {
|
|
69
|
+
child.traverse(callback);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Like traverse, but skips objects with `visible = false` and their
|
|
75
|
+
* whole subtree — hiding a group hides everything inside it. The
|
|
76
|
+
* renderer and raycaster walk the scene with this.
|
|
77
|
+
*/
|
|
78
|
+
traverseVisible(callback) {
|
|
79
|
+
if (!this.visible) return;
|
|
80
|
+
callback(this);
|
|
81
|
+
for (const child of this.children) {
|
|
82
|
+
child.traverseVisible(callback);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Destroys the per-object GPU resources created by every renderer for
|
|
88
|
+
* this object and its descendants (see GpuResources), releasing memory
|
|
89
|
+
* right away instead of waiting for GC. Geometry buffers are left alone:
|
|
90
|
+
* geometries may be shared between objects. Drawing a disposed object
|
|
91
|
+
* again re-creates its resources and re-uploads its instance data.
|
|
92
|
+
*/
|
|
93
|
+
dispose() {
|
|
94
|
+
this.traverse((object) => {
|
|
95
|
+
disposeObjectGpuResources(object);
|
|
96
|
+
// Re-created instance buffers must receive the CPU-side data before use.
|
|
97
|
+
if (object.isInstanced) object.needsUpdate = true;
|
|
98
|
+
});
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DEPTH_COMPARE,
|
|
3
|
+
INDEX_FORMAT,
|
|
4
|
+
SHADER_ENTRY_POINT,
|
|
5
|
+
STRAIGHT_ALPHA_BLEND,
|
|
6
|
+
isStripTopology,
|
|
7
|
+
} from '../../core/pipelineConstants.js';
|
|
8
|
+
import {
|
|
9
|
+
createMaterialPipelineLayouts,
|
|
10
|
+
} from '../../core/createMaterialPipelineLayouts.js';
|
|
11
|
+
import {
|
|
12
|
+
DEPTH_FORMAT,
|
|
13
|
+
SINGLE_SAMPLE_COUNT,
|
|
14
|
+
} from '../../core/rendererConfig.js';
|
|
15
|
+
import { materialUsesMap } from '../../core/materialResources.js';
|
|
16
|
+
import { vertexBufferLayouts } from '../shaders/vertexLayout.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compiles and caches the render pipelines for materials, and owns the
|
|
20
|
+
* bind group / pipeline layouts every shader shares (the
|
|
21
|
+
* @group(0) / @group(1) uniform interface in Material).
|
|
22
|
+
*
|
|
23
|
+
* A pipeline is keyed by composed shader source + pipeline state (topology,
|
|
24
|
+
* cull mode, front face, textured / instanced / transparent or not):
|
|
25
|
+
* each combination compiles once and is shared by every material
|
|
26
|
+
* instance that matches. Transparent variants alpha-blend like the 2D
|
|
27
|
+
* pipelines and don't write the depth buffer (the renderer draws them
|
|
28
|
+
* after the opaque meshes, sorted back-to-front).
|
|
29
|
+
*/
|
|
30
|
+
export class Pipelines {
|
|
31
|
+
constructor(device, format, sampleCount = SINGLE_SAMPLE_COUNT) {
|
|
32
|
+
this.device = device;
|
|
33
|
+
this.format = format;
|
|
34
|
+
/** MSAA sample count of the renderer's color/depth targets. */
|
|
35
|
+
this.sampleCount = sampleCount;
|
|
36
|
+
// composed WGSL -> Map of pipeline-state key -> GPURenderPipeline
|
|
37
|
+
this._cache = new Map();
|
|
38
|
+
// WGSL source -> GPUShaderModule, so pipeline variants that share a
|
|
39
|
+
// shader (e.g. the same material at another topology) compile once.
|
|
40
|
+
this._modules = new Map();
|
|
41
|
+
|
|
42
|
+
Object.assign(
|
|
43
|
+
this,
|
|
44
|
+
createMaterialPipelineLayouts(
|
|
45
|
+
device,
|
|
46
|
+
GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
47
|
+
{ shadows: true },
|
|
48
|
+
),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The render pipeline for a material's shader and fixed-function state. */
|
|
53
|
+
pipelineFor(material, instanced = false) {
|
|
54
|
+
const { topology, cullMode, frontFace } = material;
|
|
55
|
+
const shaderCode = instanced
|
|
56
|
+
? material.instancedShaderCode
|
|
57
|
+
: material.shaderCode;
|
|
58
|
+
let variants = this._cache.get(shaderCode);
|
|
59
|
+
if (!variants) {
|
|
60
|
+
variants = new Map();
|
|
61
|
+
this._cache.set(shaderCode, variants);
|
|
62
|
+
}
|
|
63
|
+
const textured = materialUsesMap(material);
|
|
64
|
+
const transparent = !!material.transparent;
|
|
65
|
+
const stateKey = `${topology}|${cullMode}|${frontFace}|${textured}|${instanced}|${transparent}`;
|
|
66
|
+
let pipeline = variants.get(stateKey);
|
|
67
|
+
if (!pipeline) {
|
|
68
|
+
pipeline = this._build(material, shaderCode, {
|
|
69
|
+
textured,
|
|
70
|
+
instanced,
|
|
71
|
+
transparent,
|
|
72
|
+
});
|
|
73
|
+
variants.set(stateKey, pipeline);
|
|
74
|
+
}
|
|
75
|
+
return pipeline;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_moduleFor(code) {
|
|
79
|
+
let module = this._modules.get(code);
|
|
80
|
+
if (!module) {
|
|
81
|
+
module = this.device.createShaderModule({ code });
|
|
82
|
+
this._modules.set(code, module);
|
|
83
|
+
}
|
|
84
|
+
return module;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_build(material, shaderCode, { textured, instanced, transparent }) {
|
|
88
|
+
const { topology, cullMode, frontFace } = material;
|
|
89
|
+
const primitive = { topology, cullMode, frontFace };
|
|
90
|
+
// Indexed draws on strip topologies must declare the index format
|
|
91
|
+
// up front; the renderer always uses uint32 indices.
|
|
92
|
+
if (isStripTopology(topology)) {
|
|
93
|
+
primitive.stripIndexFormat = INDEX_FORMAT;
|
|
94
|
+
}
|
|
95
|
+
const buffers = vertexBufferLayouts(instanced);
|
|
96
|
+
const target = { format: this.format };
|
|
97
|
+
if (transparent) {
|
|
98
|
+
target.blend = STRAIGHT_ALPHA_BLEND;
|
|
99
|
+
}
|
|
100
|
+
const module = this._moduleFor(shaderCode);
|
|
101
|
+
return this.device.createRenderPipeline({
|
|
102
|
+
layout: textured ? this.texturedPipelineLayout : this.pipelineLayout,
|
|
103
|
+
vertex: {
|
|
104
|
+
module,
|
|
105
|
+
entryPoint: SHADER_ENTRY_POINT.vertex,
|
|
106
|
+
buffers,
|
|
107
|
+
},
|
|
108
|
+
fragment: {
|
|
109
|
+
module,
|
|
110
|
+
entryPoint: SHADER_ENTRY_POINT.fragment,
|
|
111
|
+
targets: [target],
|
|
112
|
+
},
|
|
113
|
+
primitive,
|
|
114
|
+
multisample: { count: this.sampleCount },
|
|
115
|
+
depthStencil: {
|
|
116
|
+
format: DEPTH_FORMAT,
|
|
117
|
+
// Transparent meshes test against the depth buffer (opaque
|
|
118
|
+
// things still hide them) but don't write it: they draw last,
|
|
119
|
+
// back-to-front, and shouldn't mask each other.
|
|
120
|
+
depthWriteEnabled: !transparent,
|
|
121
|
+
depthCompare: DEFAULT_DEPTH_COMPARE,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { forEachIndexedTriangle } from '../../core/indexedTriangles.js';
|
|
2
|
+
import { VERTEX_SIZE } from '../geometries/Geometry.js';
|
|
3
|
+
|
|
4
|
+
const POSITION_COMPONENTS = 3;
|
|
5
|
+
const PARALLEL_TRIANGLE_EPSILON = 1e-12;
|
|
6
|
+
const BARYCENTRIC_EPSILON = 1e-7;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the nearest local indexed-triangle hit distance, or `null`.
|
|
10
|
+
* `direction` intentionally remains unnormalized: when it came from an
|
|
11
|
+
* inverse-transformed unit world ray, the returned value is a world distance.
|
|
12
|
+
*/
|
|
13
|
+
export function intersectIndexedGeometry(
|
|
14
|
+
origin,
|
|
15
|
+
direction,
|
|
16
|
+
geometry,
|
|
17
|
+
topology,
|
|
18
|
+
maxDistance,
|
|
19
|
+
) {
|
|
20
|
+
if (!intersectsBounds(origin, direction, geometry.bounds, maxDistance)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let nearestDistance = maxDistance;
|
|
25
|
+
let hit = false;
|
|
26
|
+
forEachIndexedTriangle(geometry.indices, topology, (a, b, c) => {
|
|
27
|
+
const distance = intersectTriangle(
|
|
28
|
+
origin,
|
|
29
|
+
direction,
|
|
30
|
+
geometry.vertices,
|
|
31
|
+
a,
|
|
32
|
+
b,
|
|
33
|
+
c,
|
|
34
|
+
nearestDistance,
|
|
35
|
+
);
|
|
36
|
+
if (distance === null) return false;
|
|
37
|
+
nearestDistance = distance;
|
|
38
|
+
hit = true;
|
|
39
|
+
return distance === 0;
|
|
40
|
+
});
|
|
41
|
+
return hit ? nearestDistance : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Segment-vs-AABB broad phase over the ray interval [0, maxDistance]. */
|
|
45
|
+
export function intersectsBounds(origin, direction, bounds, maxDistance) {
|
|
46
|
+
const min = bounds?.min;
|
|
47
|
+
const max = bounds?.max;
|
|
48
|
+
if (!hasFiniteBounds(min, max)) return false;
|
|
49
|
+
|
|
50
|
+
let near = 0;
|
|
51
|
+
let far = maxDistance;
|
|
52
|
+
for (let axis = 0; axis < POSITION_COMPONENTS; axis++) {
|
|
53
|
+
const component = componentAt(direction, axis);
|
|
54
|
+
const start = componentAt(origin, axis);
|
|
55
|
+
if (component === 0) {
|
|
56
|
+
if (start < min[axis] || start > max[axis]) return false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let entry = (min[axis] - start) / component;
|
|
61
|
+
let exit = (max[axis] - start) / component;
|
|
62
|
+
if (entry > exit) [entry, exit] = [exit, entry];
|
|
63
|
+
near = Math.max(near, entry);
|
|
64
|
+
far = Math.min(far, exit);
|
|
65
|
+
if (near > far) return false;
|
|
66
|
+
}
|
|
67
|
+
return far >= 0 && near <= maxDistance;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function intersectTriangle(
|
|
71
|
+
origin,
|
|
72
|
+
direction,
|
|
73
|
+
vertices,
|
|
74
|
+
indexA,
|
|
75
|
+
indexB,
|
|
76
|
+
indexC,
|
|
77
|
+
maxDistance,
|
|
78
|
+
) {
|
|
79
|
+
const offsetA = vertexOffset(vertices, indexA);
|
|
80
|
+
const offsetB = vertexOffset(vertices, indexB);
|
|
81
|
+
const offsetC = vertexOffset(vertices, indexC);
|
|
82
|
+
if (offsetA < 0 || offsetB < 0 || offsetC < 0) return null;
|
|
83
|
+
|
|
84
|
+
const ax = vertices[offsetA];
|
|
85
|
+
const ay = vertices[offsetA + 1];
|
|
86
|
+
const az = vertices[offsetA + 2];
|
|
87
|
+
const edge1x = vertices[offsetB] - ax;
|
|
88
|
+
const edge1y = vertices[offsetB + 1] - ay;
|
|
89
|
+
const edge1z = vertices[offsetB + 2] - az;
|
|
90
|
+
const edge2x = vertices[offsetC] - ax;
|
|
91
|
+
const edge2y = vertices[offsetC + 1] - ay;
|
|
92
|
+
const edge2z = vertices[offsetC + 2] - az;
|
|
93
|
+
if (
|
|
94
|
+
!Number.isFinite(ax) ||
|
|
95
|
+
!Number.isFinite(ay) ||
|
|
96
|
+
!Number.isFinite(az) ||
|
|
97
|
+
!Number.isFinite(edge1x) ||
|
|
98
|
+
!Number.isFinite(edge1y) ||
|
|
99
|
+
!Number.isFinite(edge1z) ||
|
|
100
|
+
!Number.isFinite(edge2x) ||
|
|
101
|
+
!Number.isFinite(edge2y) ||
|
|
102
|
+
!Number.isFinite(edge2z)
|
|
103
|
+
) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const px = direction.y * edge2z - direction.z * edge2y;
|
|
108
|
+
const py = direction.z * edge2x - direction.x * edge2z;
|
|
109
|
+
const pz = direction.x * edge2y - direction.y * edge2x;
|
|
110
|
+
const determinant = edge1x * px + edge1y * py + edge1z * pz;
|
|
111
|
+
const determinantScale =
|
|
112
|
+
Math.hypot(edge1x, edge1y, edge1z) *
|
|
113
|
+
Math.hypot(edge2x, edge2y, edge2z) *
|
|
114
|
+
Math.hypot(direction.x, direction.y, direction.z);
|
|
115
|
+
if (
|
|
116
|
+
!Number.isFinite(determinantScale) ||
|
|
117
|
+
determinantScale === 0 ||
|
|
118
|
+
Math.abs(determinant) <=
|
|
119
|
+
determinantScale * PARALLEL_TRIANGLE_EPSILON
|
|
120
|
+
) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const inverseDeterminant = 1 / determinant;
|
|
125
|
+
const tx = origin.x - ax;
|
|
126
|
+
const ty = origin.y - ay;
|
|
127
|
+
const tz = origin.z - az;
|
|
128
|
+
const u = (tx * px + ty * py + tz * pz) * inverseDeterminant;
|
|
129
|
+
if (u < -BARYCENTRIC_EPSILON || u > 1 + BARYCENTRIC_EPSILON) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const qx = ty * edge1z - tz * edge1y;
|
|
134
|
+
const qy = tz * edge1x - tx * edge1z;
|
|
135
|
+
const qz = tx * edge1y - ty * edge1x;
|
|
136
|
+
const v =
|
|
137
|
+
(direction.x * qx + direction.y * qy + direction.z * qz) *
|
|
138
|
+
inverseDeterminant;
|
|
139
|
+
if (
|
|
140
|
+
v < -BARYCENTRIC_EPSILON ||
|
|
141
|
+
u + v > 1 + BARYCENTRIC_EPSILON
|
|
142
|
+
) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const distance =
|
|
147
|
+
(edge2x * qx + edge2y * qy + edge2z * qz) * inverseDeterminant;
|
|
148
|
+
if (
|
|
149
|
+
!Number.isFinite(distance) ||
|
|
150
|
+
distance < 0 ||
|
|
151
|
+
distance > maxDistance
|
|
152
|
+
) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return distance;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function vertexOffset(vertices, index) {
|
|
159
|
+
if (!Number.isInteger(index)) return -1;
|
|
160
|
+
const offset = index * VERTEX_SIZE;
|
|
161
|
+
return index >= 0 && offset + POSITION_COMPONENTS <= vertices.length
|
|
162
|
+
? offset
|
|
163
|
+
: -1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function hasFiniteBounds(min, max) {
|
|
167
|
+
if (!min || !max || min.length < 3 || max.length < 3) return false;
|
|
168
|
+
for (let axis = 0; axis < POSITION_COMPONENTS; axis++) {
|
|
169
|
+
if (
|
|
170
|
+
!Number.isFinite(min[axis]) ||
|
|
171
|
+
!Number.isFinite(max[axis]) ||
|
|
172
|
+
min[axis] > max[axis]
|
|
173
|
+
) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function componentAt(vector, axis) {
|
|
181
|
+
if (axis === 0) return vector.x;
|
|
182
|
+
if (axis === 1) return vector.y;
|
|
183
|
+
return vector.z;
|
|
184
|
+
}
|