@multiplekex/shallot 0.1.5 → 0.1.6
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/package.json +1 -1
- package/src/extras/arrows/index.ts +64 -45
- package/src/extras/lines/index.ts +14 -2
- package/src/extras/text/index.ts +29 -13
- package/src/standard/compute/graph.ts +6 -3
- package/src/standard/compute/index.ts +9 -0
- package/src/standard/compute/inspect.ts +3 -3
- package/src/standard/input/index.ts +61 -17
- package/src/standard/render/light.ts +2 -2
- package/src/standard/render/postprocess.ts +23 -24
- package/src/standard/render/scene.ts +1 -18
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ComputeNode,
|
|
8
8
|
type ExecutionContext,
|
|
9
9
|
} from "../../standard/compute";
|
|
10
|
-
import { Render, RenderPlugin } from "../../standard/render";
|
|
10
|
+
import { Render, RenderPlugin, DEPTH_FORMAT } from "../../standard/render";
|
|
11
11
|
import { Transform } from "../../standard/transforms";
|
|
12
12
|
import { Line, Lines, LinesPlugin } from "../lines";
|
|
13
13
|
|
|
@@ -125,65 +125,72 @@ fn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) iid: u32) -> Ver
|
|
|
125
125
|
let line = lines[eid];
|
|
126
126
|
let transform = matrices[eid];
|
|
127
127
|
|
|
128
|
+
// Extract scale from transform matrix
|
|
129
|
+
let scaleX = length(transform[0].xyz);
|
|
130
|
+
let scaleY = length(transform[1].xyz);
|
|
131
|
+
let scaleZ = length(transform[2].xyz);
|
|
132
|
+
let avgScale = (scaleX + scaleY + scaleZ) / 3.0;
|
|
133
|
+
|
|
134
|
+
// Skip rendering if scale is near zero
|
|
135
|
+
if avgScale < 0.001 {
|
|
136
|
+
var out: VertexOutput;
|
|
137
|
+
out.position = vec4(0.0, 0.0, -2.0, 1.0);
|
|
138
|
+
out.color = vec4(0.0);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
128
142
|
let start = transform[3].xyz;
|
|
129
143
|
let rotation = mat3x3<f32>(transform[0].xyz, transform[1].xyz, transform[2].xyz);
|
|
130
144
|
let end = start + rotation * line.offset;
|
|
131
145
|
|
|
132
|
-
//
|
|
133
|
-
let
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
// Convert to NDC
|
|
137
|
-
let startNDC = startClip.xy / startClip.w;
|
|
138
|
-
let endNDC = endClip.xy / endClip.w;
|
|
139
|
-
|
|
140
|
-
// Direction in screen space
|
|
141
|
-
let dir = endNDC - startNDC;
|
|
142
|
-
let len = length(dir);
|
|
143
|
-
let normDir = select(vec2(1.0, 0.0), dir / len, len > 0.0001);
|
|
146
|
+
// Line direction in world space
|
|
147
|
+
let lineVec = end - start;
|
|
148
|
+
let lineLen = length(lineVec);
|
|
149
|
+
let lineDir = select(vec3(1.0, 0.0, 0.0), lineVec / lineLen, lineLen > 0.0001);
|
|
144
150
|
|
|
145
|
-
//
|
|
146
|
-
let
|
|
151
|
+
// Camera position and direction to arrow
|
|
152
|
+
let cameraPos = scene.cameraWorld[3].xyz;
|
|
153
|
+
let anchorWorld = select(start, end, isEnd);
|
|
154
|
+
let toCamera = normalize(cameraPos - anchorWorld);
|
|
155
|
+
|
|
156
|
+
// Billboard perpendicular: perpendicular to both line and view direction
|
|
157
|
+
var perp = cross(lineDir, toCamera);
|
|
158
|
+
let perpLen = length(perp);
|
|
159
|
+
if perpLen < 0.001 {
|
|
160
|
+
// Line pointing at camera, pick arbitrary perpendicular
|
|
161
|
+
let up = select(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), abs(lineDir.y) > 0.9);
|
|
162
|
+
perp = normalize(cross(lineDir, up));
|
|
163
|
+
} else {
|
|
164
|
+
perp = perp / perpLen;
|
|
165
|
+
}
|
|
147
166
|
|
|
148
167
|
// Arrow size scales with sqrt of line thickness and world-space distance to camera
|
|
149
|
-
let
|
|
150
|
-
let midpoint = (start + end) * 0.5;
|
|
151
|
-
let distToCamera = length(cameraPos - midpoint);
|
|
168
|
+
let distToCamera = length(cameraPos - anchorWorld);
|
|
152
169
|
let refDist = 15.0;
|
|
153
170
|
let rawScale = distToCamera / refDist;
|
|
154
171
|
let maxScale = 3.0;
|
|
155
172
|
let zoomScale = max(1.0, rawScale / (1.0 + rawScale / maxScale));
|
|
156
|
-
let baseSize = sqrt(line.thickness) * 0.
|
|
157
|
-
let arrowLength = arrow.size * baseSize *
|
|
158
|
-
let arrowWidth = arrow.size * baseSize *
|
|
159
|
-
|
|
160
|
-
// Pick anchor position and direction based on which end
|
|
161
|
-
var anchorNDC: vec2<f32>;
|
|
162
|
-
var anchorDepth: f32;
|
|
163
|
-
var arrowDir: vec2<f32>;
|
|
164
|
-
|
|
165
|
-
if isEnd {
|
|
166
|
-
anchorNDC = endNDC;
|
|
167
|
-
anchorDepth = endClip.z / endClip.w;
|
|
168
|
-
arrowDir = normDir;
|
|
169
|
-
} else {
|
|
170
|
-
anchorNDC = startNDC;
|
|
171
|
-
anchorDepth = startClip.z / startClip.w;
|
|
172
|
-
arrowDir = -normDir;
|
|
173
|
-
}
|
|
173
|
+
let baseSize = sqrt(line.thickness) * 0.1 * zoomScale;
|
|
174
|
+
let arrowLength = arrow.size * baseSize * 2.0;
|
|
175
|
+
let arrowWidth = arrow.size * baseSize * 0.8;
|
|
174
176
|
|
|
175
|
-
//
|
|
176
|
-
let
|
|
177
|
-
|
|
177
|
+
// Arrow direction along line
|
|
178
|
+
let arrowDir = select(-lineDir, lineDir, isEnd);
|
|
179
|
+
|
|
180
|
+
// Build triangle vertices in world space
|
|
181
|
+
var worldPos: vec3<f32>;
|
|
178
182
|
switch vid {
|
|
179
|
-
case 0u: {
|
|
180
|
-
case 1u: {
|
|
181
|
-
case 2u: {
|
|
182
|
-
default: {
|
|
183
|
+
case 0u: { worldPos = anchorWorld + arrowDir * arrowLength * 0.5; }
|
|
184
|
+
case 1u: { worldPos = anchorWorld - arrowDir * arrowLength * 0.5 + perp * arrowWidth; }
|
|
185
|
+
case 2u: { worldPos = anchorWorld - arrowDir * arrowLength * 0.5 - perp * arrowWidth; }
|
|
186
|
+
default: { worldPos = anchorWorld; }
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
// Project to clip space
|
|
190
|
+
let clipPos = scene.viewProj * vec4(worldPos, 1.0);
|
|
191
|
+
|
|
185
192
|
var out: VertexOutput;
|
|
186
|
-
out.position =
|
|
193
|
+
out.position = clipPos;
|
|
187
194
|
out.color = vec4(line.color.rgb, line.color.a * line.opacity);
|
|
188
195
|
return out;
|
|
189
196
|
}
|
|
@@ -230,6 +237,11 @@ export function createArrowsPipeline(
|
|
|
230
237
|
primitive: {
|
|
231
238
|
topology: "triangle-list",
|
|
232
239
|
},
|
|
240
|
+
depthStencil: {
|
|
241
|
+
format: DEPTH_FORMAT,
|
|
242
|
+
depthCompare: "less",
|
|
243
|
+
depthWriteEnabled: false,
|
|
244
|
+
},
|
|
233
245
|
});
|
|
234
246
|
}
|
|
235
247
|
|
|
@@ -239,7 +251,7 @@ export function createArrowsNode(config: ArrowsConfig): ComputeNode {
|
|
|
239
251
|
|
|
240
252
|
return {
|
|
241
253
|
id: "arrows",
|
|
242
|
-
phase: "
|
|
254
|
+
phase: "transparent",
|
|
243
255
|
inputs: [],
|
|
244
256
|
outputs: [],
|
|
245
257
|
|
|
@@ -267,6 +279,8 @@ export function createArrowsNode(config: ArrowsConfig): ComputeNode {
|
|
|
267
279
|
});
|
|
268
280
|
}
|
|
269
281
|
|
|
282
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
283
|
+
|
|
270
284
|
const pass = encoder.beginRenderPass({
|
|
271
285
|
colorAttachments: [
|
|
272
286
|
{
|
|
@@ -275,6 +289,11 @@ export function createArrowsNode(config: ArrowsConfig): ComputeNode {
|
|
|
275
289
|
storeOp: "store" as const,
|
|
276
290
|
},
|
|
277
291
|
],
|
|
292
|
+
depthStencilAttachment: {
|
|
293
|
+
view: depthView,
|
|
294
|
+
depthLoadOp: "load" as const,
|
|
295
|
+
depthStoreOp: "store" as const,
|
|
296
|
+
},
|
|
278
297
|
});
|
|
279
298
|
|
|
280
299
|
pass.setPipeline(pipeline);
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ComputeNode,
|
|
8
8
|
type ExecutionContext,
|
|
9
9
|
} from "../../standard/compute";
|
|
10
|
-
import { Render, RenderPlugin } from "../../standard/render";
|
|
10
|
+
import { Render, RenderPlugin, DEPTH_FORMAT } from "../../standard/render";
|
|
11
11
|
import { Transform } from "../../standard/transforms";
|
|
12
12
|
|
|
13
13
|
export const LineData = {
|
|
@@ -245,6 +245,11 @@ export function createLinesPipeline(
|
|
|
245
245
|
primitive: {
|
|
246
246
|
topology: "triangle-list",
|
|
247
247
|
},
|
|
248
|
+
depthStencil: {
|
|
249
|
+
format: DEPTH_FORMAT,
|
|
250
|
+
depthCompare: "less",
|
|
251
|
+
depthWriteEnabled: false,
|
|
252
|
+
},
|
|
248
253
|
});
|
|
249
254
|
}
|
|
250
255
|
|
|
@@ -254,7 +259,7 @@ export function createLinesNode(config: LinesConfig): ComputeNode {
|
|
|
254
259
|
|
|
255
260
|
return {
|
|
256
261
|
id: "lines",
|
|
257
|
-
phase: "
|
|
262
|
+
phase: "transparent",
|
|
258
263
|
inputs: [],
|
|
259
264
|
outputs: [],
|
|
260
265
|
|
|
@@ -281,6 +286,8 @@ export function createLinesNode(config: LinesConfig): ComputeNode {
|
|
|
281
286
|
});
|
|
282
287
|
}
|
|
283
288
|
|
|
289
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
290
|
+
|
|
284
291
|
const pass = encoder.beginRenderPass({
|
|
285
292
|
colorAttachments: [
|
|
286
293
|
{
|
|
@@ -289,6 +296,11 @@ export function createLinesNode(config: LinesConfig): ComputeNode {
|
|
|
289
296
|
storeOp: "store" as const,
|
|
290
297
|
},
|
|
291
298
|
],
|
|
299
|
+
depthStencilAttachment: {
|
|
300
|
+
view: depthView,
|
|
301
|
+
depthLoadOp: "load" as const,
|
|
302
|
+
depthStoreOp: "store" as const,
|
|
303
|
+
},
|
|
292
304
|
});
|
|
293
305
|
|
|
294
306
|
pass.setPipeline(pipeline);
|
package/src/extras/text/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type ComputeNode,
|
|
8
8
|
type ExecutionContext,
|
|
9
9
|
} from "../../standard/compute";
|
|
10
|
-
import { Render, RenderPlugin } from "../../standard/render";
|
|
10
|
+
import { Render, RenderPlugin, DEPTH_FORMAT } from "../../standard/render";
|
|
11
11
|
import { Transform } from "../../standard/transforms";
|
|
12
12
|
|
|
13
13
|
const MAX_GLYPHS = 50000;
|
|
@@ -304,7 +304,7 @@ struct GlyphInstance {
|
|
|
304
304
|
posX: f32,
|
|
305
305
|
posY: f32,
|
|
306
306
|
posZ: f32,
|
|
307
|
-
|
|
307
|
+
entityId: u32,
|
|
308
308
|
width: f32,
|
|
309
309
|
height: f32,
|
|
310
310
|
_pad1: vec2<f32>,
|
|
@@ -319,6 +319,7 @@ struct GlyphInstance {
|
|
|
319
319
|
@group(0) @binding(1) var<storage, read> glyphs: array<GlyphInstance>;
|
|
320
320
|
@group(0) @binding(2) var atlasTexture: texture_2d<f32>;
|
|
321
321
|
@group(0) @binding(3) var atlasSampler: sampler;
|
|
322
|
+
@group(0) @binding(4) var<storage, read> matrices: array<mat4x4<f32>>;
|
|
322
323
|
|
|
323
324
|
struct VertexOutput {
|
|
324
325
|
@builtin(position) position: vec4<f32>,
|
|
@@ -369,14 +370,17 @@ fn vs(@builtin(vertex_index) vid: u32) -> VertexOutput {
|
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
let
|
|
373
|
+
let localPos3 = vec3(
|
|
373
374
|
glyph.posX + localPos.x * glyph.width,
|
|
374
375
|
glyph.posY + localPos.y * glyph.height,
|
|
375
376
|
glyph.posZ
|
|
376
377
|
);
|
|
377
378
|
|
|
379
|
+
let transform = matrices[glyph.entityId];
|
|
380
|
+
let worldPos = transform * vec4(localPos3, 1.0);
|
|
381
|
+
|
|
378
382
|
var out: VertexOutput;
|
|
379
|
-
out.position = scene.viewProj *
|
|
383
|
+
out.position = scene.viewProj * worldPos;
|
|
380
384
|
out.uv = uv;
|
|
381
385
|
out.color = glyph.color;
|
|
382
386
|
return out;
|
|
@@ -429,6 +433,11 @@ function createTextPipeline(device: GPUDevice, format: GPUTextureFormat): GPURen
|
|
|
429
433
|
topology: "triangle-list",
|
|
430
434
|
cullMode: "none",
|
|
431
435
|
},
|
|
436
|
+
depthStencil: {
|
|
437
|
+
format: DEPTH_FORMAT,
|
|
438
|
+
depthCompare: "less",
|
|
439
|
+
depthWriteEnabled: false,
|
|
440
|
+
},
|
|
432
441
|
});
|
|
433
442
|
}
|
|
434
443
|
|
|
@@ -437,6 +446,7 @@ export interface TextConfig {
|
|
|
437
446
|
glyphs: GPUBuffer;
|
|
438
447
|
atlas: GPUTextureView;
|
|
439
448
|
sampler: GPUSampler;
|
|
449
|
+
matrices: GPUBuffer;
|
|
440
450
|
getCount: () => number;
|
|
441
451
|
}
|
|
442
452
|
|
|
@@ -446,7 +456,7 @@ export function createTextNode(config: TextConfig): ComputeNode {
|
|
|
446
456
|
|
|
447
457
|
return {
|
|
448
458
|
id: "text",
|
|
449
|
-
phase: "
|
|
459
|
+
phase: "transparent",
|
|
450
460
|
inputs: [],
|
|
451
461
|
outputs: [],
|
|
452
462
|
|
|
@@ -469,10 +479,13 @@ export function createTextNode(config: TextConfig): ComputeNode {
|
|
|
469
479
|
{ binding: 1, resource: { buffer: config.glyphs } },
|
|
470
480
|
{ binding: 2, resource: config.atlas },
|
|
471
481
|
{ binding: 3, resource: config.sampler },
|
|
482
|
+
{ binding: 4, resource: { buffer: config.matrices } },
|
|
472
483
|
],
|
|
473
484
|
});
|
|
474
485
|
}
|
|
475
486
|
|
|
487
|
+
const depthView = ctx.getTextureView("depth")!;
|
|
488
|
+
|
|
476
489
|
const pass = encoder.beginRenderPass({
|
|
477
490
|
colorAttachments: [
|
|
478
491
|
{
|
|
@@ -481,6 +494,11 @@ export function createTextNode(config: TextConfig): ComputeNode {
|
|
|
481
494
|
storeOp: "store" as const,
|
|
482
495
|
},
|
|
483
496
|
],
|
|
497
|
+
depthStencilAttachment: {
|
|
498
|
+
view: depthView,
|
|
499
|
+
depthLoadOp: "load" as const,
|
|
500
|
+
depthStoreOp: "store" as const,
|
|
501
|
+
},
|
|
484
502
|
});
|
|
485
503
|
|
|
486
504
|
pass.setPipeline(pipeline);
|
|
@@ -523,6 +541,7 @@ const TextSystem: System = {
|
|
|
523
541
|
|
|
524
542
|
const { device } = compute;
|
|
525
543
|
const { atlas, staging, content } = text;
|
|
544
|
+
const stagingU32 = new Uint32Array(staging.buffer);
|
|
526
545
|
|
|
527
546
|
let glyphCount = 0;
|
|
528
547
|
|
|
@@ -542,10 +561,6 @@ const TextSystem: System = {
|
|
|
542
561
|
const offsetX = -layout.width * anchorX;
|
|
543
562
|
const offsetY = -layout.height * anchorY;
|
|
544
563
|
|
|
545
|
-
const baseX = Transform.posX[eid] + offsetX;
|
|
546
|
-
const baseY = Transform.posY[eid] + offsetY;
|
|
547
|
-
const baseZ = Transform.posZ[eid];
|
|
548
|
-
|
|
549
564
|
const color = Text.color[eid];
|
|
550
565
|
const r = ((color >> 16) & 0xff) / 255;
|
|
551
566
|
const g = ((color >> 8) & 0xff) / 255;
|
|
@@ -557,10 +572,10 @@ const TextSystem: System = {
|
|
|
557
572
|
|
|
558
573
|
const offset = glyphCount * GLYPH_FLOATS;
|
|
559
574
|
|
|
560
|
-
staging[offset + 0] =
|
|
561
|
-
staging[offset + 1] =
|
|
562
|
-
staging[offset + 2] =
|
|
563
|
-
|
|
575
|
+
staging[offset + 0] = offsetX + glyph.x;
|
|
576
|
+
staging[offset + 1] = offsetY + glyph.y;
|
|
577
|
+
staging[offset + 2] = 0;
|
|
578
|
+
stagingU32[offset + 3] = eid;
|
|
564
579
|
|
|
565
580
|
staging[offset + 4] = glyph.width;
|
|
566
581
|
staging[offset + 5] = glyph.height;
|
|
@@ -634,6 +649,7 @@ export const TextPlugin: Plugin = {
|
|
|
634
649
|
glyphs: textState.buffer,
|
|
635
650
|
atlas: atlas.textureView,
|
|
636
651
|
sampler,
|
|
652
|
+
matrices: render.matrices,
|
|
637
653
|
getCount: () => textState.count,
|
|
638
654
|
})
|
|
639
655
|
);
|
|
@@ -3,9 +3,9 @@ import { inspect as inspectGraph, type GraphInspection } from "./inspect";
|
|
|
3
3
|
|
|
4
4
|
export type ResourceId = string;
|
|
5
5
|
export type NodeId = string;
|
|
6
|
-
export type Phase = "
|
|
6
|
+
export type Phase = "opaque" | "transparent" | "postprocess";
|
|
7
7
|
|
|
8
|
-
const PHASE_ORDER: Phase[] = ["
|
|
8
|
+
const PHASE_ORDER: Phase[] = ["opaque", "transparent", "postprocess"];
|
|
9
9
|
|
|
10
10
|
export interface ResourceRef {
|
|
11
11
|
id: ResourceId;
|
|
@@ -22,6 +22,9 @@ export interface ExecutionContext {
|
|
|
22
22
|
getTexture(id: ResourceId): GPUTexture | null;
|
|
23
23
|
getTextureView(id: ResourceId): GPUTextureView | null;
|
|
24
24
|
getBuffer(id: ResourceId): GPUBuffer | null;
|
|
25
|
+
setTexture(id: ResourceId, texture: GPUTexture): void;
|
|
26
|
+
setTextureView(id: ResourceId, view: GPUTextureView): void;
|
|
27
|
+
setBuffer(id: ResourceId, buffer: GPUBuffer): void;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export interface ComputeNode {
|
|
@@ -116,7 +119,7 @@ function compile(nodes: ComputeNode[]): ExecutionPlan {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
for (const node of nodes) {
|
|
119
|
-
const phase = node.phase ?? "
|
|
122
|
+
const phase = node.phase ?? "opaque";
|
|
120
123
|
byPhase.get(phase)!.push(node);
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -105,6 +105,15 @@ export const ComputeSystem: System = {
|
|
|
105
105
|
getBuffer(id: ResourceId) {
|
|
106
106
|
return resources.buffers.get(id) ?? null;
|
|
107
107
|
},
|
|
108
|
+
setTexture(id: ResourceId, texture: GPUTexture) {
|
|
109
|
+
resources.textures.set(id, texture);
|
|
110
|
+
},
|
|
111
|
+
setTextureView(id: ResourceId, view: GPUTextureView) {
|
|
112
|
+
resources.textureViews.set(id, view);
|
|
113
|
+
},
|
|
114
|
+
setBuffer(id: ResourceId, buffer: GPUBuffer) {
|
|
115
|
+
resources.buffers.set(id, buffer);
|
|
116
|
+
},
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
for (const node of plan.sorted) {
|
|
@@ -20,12 +20,12 @@ export interface GraphInspection {
|
|
|
20
20
|
readonly byPhase: ReadonlyMap<Phase, readonly NodeId[]>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const PHASE_ORDER: Phase[] = ["
|
|
23
|
+
const PHASE_ORDER: Phase[] = ["opaque", "transparent", "postprocess"];
|
|
24
24
|
|
|
25
25
|
export function inspect(nodes: readonly ComputeNode[]): GraphInspection {
|
|
26
26
|
const nodeInfos: NodeInfo[] = nodes.map((n) => ({
|
|
27
27
|
id: n.id,
|
|
28
|
-
phase: n.phase ?? "
|
|
28
|
+
phase: n.phase ?? "opaque",
|
|
29
29
|
inputs: n.inputs,
|
|
30
30
|
outputs: n.outputs,
|
|
31
31
|
}));
|
|
@@ -97,7 +97,7 @@ function topoSortIds(nodes: readonly ComputeNode[]): NodeId[] {
|
|
|
97
97
|
byPhase.set(phase, []);
|
|
98
98
|
}
|
|
99
99
|
for (const node of nodes) {
|
|
100
|
-
const phase = node.phase ?? "
|
|
100
|
+
const phase = node.phase ?? "opaque";
|
|
101
101
|
byPhase.get(phase)!.push(node);
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -30,6 +30,10 @@ const inputState: InputState = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
let canvas: HTMLCanvasElement | null = null;
|
|
33
|
+
let lastPointerX = 0;
|
|
34
|
+
let lastPointerY = 0;
|
|
35
|
+
let activePointerId: number | null = null;
|
|
36
|
+
let activeButton: number | null = null;
|
|
33
37
|
|
|
34
38
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
35
39
|
if (!keys.has(e.code)) {
|
|
@@ -43,22 +47,55 @@ function handleKeyUp(e: KeyboardEvent): void {
|
|
|
43
47
|
keysReleased.add(e.code);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
function
|
|
50
|
+
function setButtonState(button: number, pressed: boolean): void {
|
|
51
|
+
if (button === 0) mouse.left = pressed;
|
|
52
|
+
if (button === 1) mouse.middle = pressed;
|
|
53
|
+
if (button === 2) mouse.right = pressed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function clearPointerState(): void {
|
|
57
|
+
if (activeButton !== null) {
|
|
58
|
+
setButtonState(activeButton, false);
|
|
59
|
+
}
|
|
60
|
+
activePointerId = null;
|
|
61
|
+
activeButton = null;
|
|
62
|
+
lastPointerX = 0;
|
|
63
|
+
lastPointerY = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handlePointerDown(e: PointerEvent): void {
|
|
47
67
|
if (e.target !== canvas) return;
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
if (activePointerId === null) {
|
|
69
|
+
activePointerId = e.pointerId;
|
|
70
|
+
activeButton = e.button;
|
|
71
|
+
lastPointerX = e.clientX;
|
|
72
|
+
lastPointerY = e.clientY;
|
|
73
|
+
setButtonState(e.button, true);
|
|
74
|
+
canvas!.setPointerCapture(e.pointerId);
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handlePointerUp(e: PointerEvent): void {
|
|
80
|
+
if (e.pointerId === activePointerId) {
|
|
81
|
+
canvas?.releasePointerCapture(e.pointerId);
|
|
82
|
+
clearPointerState();
|
|
83
|
+
}
|
|
51
84
|
}
|
|
52
85
|
|
|
53
|
-
function
|
|
54
|
-
if (e.
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
function handlePointerCancel(e: PointerEvent): void {
|
|
87
|
+
if (e.pointerId === activePointerId) {
|
|
88
|
+
clearPointerState();
|
|
89
|
+
}
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
function handlePointerMove(e: PointerEvent): void {
|
|
93
|
+
if (e.pointerId !== activePointerId) return;
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
mouse.deltaX += e.clientX - lastPointerX;
|
|
96
|
+
mouse.deltaY += e.clientY - lastPointerY;
|
|
97
|
+
lastPointerX = e.clientX;
|
|
98
|
+
lastPointerY = e.clientY;
|
|
62
99
|
}
|
|
63
100
|
|
|
64
101
|
function handleWheel(e: WheelEvent): void {
|
|
@@ -91,6 +128,10 @@ function clearAllState(): void {
|
|
|
91
128
|
mouse.left = false;
|
|
92
129
|
mouse.right = false;
|
|
93
130
|
mouse.middle = false;
|
|
131
|
+
activePointerId = null;
|
|
132
|
+
activeButton = null;
|
|
133
|
+
lastPointerX = 0;
|
|
134
|
+
lastPointerY = 0;
|
|
94
135
|
}
|
|
95
136
|
|
|
96
137
|
export const InputSystem: System = {
|
|
@@ -99,12 +140,14 @@ export const InputSystem: System = {
|
|
|
99
140
|
setup(state: State) {
|
|
100
141
|
if (!state.canvas) return;
|
|
101
142
|
canvas = state.canvas;
|
|
143
|
+
canvas.style.touchAction = "none";
|
|
102
144
|
|
|
103
145
|
window.addEventListener("keydown", handleKeyDown);
|
|
104
146
|
window.addEventListener("keyup", handleKeyUp);
|
|
105
|
-
canvas.addEventListener("
|
|
106
|
-
window.addEventListener("
|
|
107
|
-
|
|
147
|
+
canvas.addEventListener("pointerdown", handlePointerDown);
|
|
148
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
149
|
+
window.addEventListener("pointercancel", handlePointerCancel);
|
|
150
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
108
151
|
canvas.addEventListener("wheel", handleWheel, { passive: false });
|
|
109
152
|
canvas.addEventListener("contextmenu", handleContextMenu);
|
|
110
153
|
|
|
@@ -115,9 +158,10 @@ export const InputSystem: System = {
|
|
|
115
158
|
if (canvas) {
|
|
116
159
|
window.removeEventListener("keydown", handleKeyDown);
|
|
117
160
|
window.removeEventListener("keyup", handleKeyUp);
|
|
118
|
-
canvas.removeEventListener("
|
|
119
|
-
window.removeEventListener("
|
|
120
|
-
|
|
161
|
+
canvas.removeEventListener("pointerdown", handlePointerDown);
|
|
162
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
163
|
+
window.removeEventListener("pointercancel", handlePointerCancel);
|
|
164
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
121
165
|
canvas.removeEventListener("wheel", handleWheel);
|
|
122
166
|
canvas.removeEventListener("contextmenu", handleContextMenu);
|
|
123
167
|
canvas = null;
|
|
@@ -6,26 +6,6 @@ struct VertexOutput {
|
|
|
6
6
|
@location(0) uv: vec2f,
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
@vertex
|
|
10
|
-
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
11
|
-
var positions = array<vec2f, 3>(
|
|
12
|
-
vec2f(-1.0, -1.0),
|
|
13
|
-
vec2f(3.0, -1.0),
|
|
14
|
-
vec2f(-1.0, 3.0)
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
let pos = positions[vertexIndex];
|
|
18
|
-
|
|
19
|
-
var output: VertexOutput;
|
|
20
|
-
output.position = vec4f(pos, 0.0, 1.0);
|
|
21
|
-
output.uv = (pos + 1.0) * 0.5;
|
|
22
|
-
output.uv.y = 1.0 - output.uv.y;
|
|
23
|
-
return output;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
27
|
-
@group(0) @binding(1) var inputSampler: sampler;
|
|
28
|
-
|
|
29
9
|
struct Uniforms {
|
|
30
10
|
exposure: f32,
|
|
31
11
|
vignetteStrength: f32,
|
|
@@ -37,12 +17,31 @@ struct Uniforms {
|
|
|
37
17
|
_pad: u32,
|
|
38
18
|
}
|
|
39
19
|
|
|
20
|
+
@group(0) @binding(0) var inputTexture: texture_2d<f32>;
|
|
21
|
+
@group(0) @binding(1) var inputSampler: sampler;
|
|
40
22
|
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
41
23
|
|
|
42
24
|
const FLAG_TONEMAP: u32 = 1u;
|
|
43
25
|
const FLAG_FXAA: u32 = 2u;
|
|
44
26
|
const FLAG_VIGNETTE: u32 = 4u;
|
|
45
27
|
|
|
28
|
+
@vertex
|
|
29
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
30
|
+
var positions = array<vec2f, 3>(
|
|
31
|
+
vec2f(-1.0, -1.0),
|
|
32
|
+
vec2f(3.0, -1.0),
|
|
33
|
+
vec2f(-1.0, 3.0)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
let pos = positions[vertexIndex];
|
|
37
|
+
|
|
38
|
+
var output: VertexOutput;
|
|
39
|
+
output.position = vec4f(pos, 0.0, 1.0);
|
|
40
|
+
output.uv = (pos + 1.0) * 0.5;
|
|
41
|
+
output.uv.y = 1.0 - output.uv.y;
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
|
|
46
45
|
fn aces(x: vec3f) -> vec3f {
|
|
47
46
|
let a = 2.51;
|
|
48
47
|
let b = 0.03;
|
|
@@ -111,11 +110,11 @@ fn applyVignette(color: vec3f, uv: vec2f) -> vec3f {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
@fragment
|
|
114
|
-
fn fragmentMain(
|
|
115
|
-
var color = textureSample(inputTexture, inputSampler, uv).rgb;
|
|
113
|
+
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
|
|
114
|
+
var color = textureSample(inputTexture, inputSampler, input.uv).rgb;
|
|
116
115
|
|
|
117
116
|
if (uniforms.flags & FLAG_FXAA) != 0u {
|
|
118
|
-
color = applyFXAA(uv, color);
|
|
117
|
+
color = applyFXAA(input.uv, color);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
if (uniforms.flags & FLAG_TONEMAP) != 0u {
|
|
@@ -123,7 +122,7 @@ fn fragmentMain(@location(0) uv: vec2f) -> @location(0) vec4f {
|
|
|
123
122
|
}
|
|
124
123
|
|
|
125
124
|
if (uniforms.flags & FLAG_VIGNETTE) != 0u {
|
|
126
|
-
color = applyVignette(color, uv);
|
|
125
|
+
color = applyVignette(color, input.uv);
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
return vec4f(color, 1.0);
|
|
@@ -75,24 +75,7 @@ export function orthographic(
|
|
|
75
75
|
const lr = 1 / (size * aspect);
|
|
76
76
|
const bt = 1 / size;
|
|
77
77
|
const nf = 1 / (near - far);
|
|
78
|
-
return new Float32Array([
|
|
79
|
-
lr,
|
|
80
|
-
0,
|
|
81
|
-
0,
|
|
82
|
-
0,
|
|
83
|
-
0,
|
|
84
|
-
bt,
|
|
85
|
-
0,
|
|
86
|
-
0,
|
|
87
|
-
0,
|
|
88
|
-
0,
|
|
89
|
-
nf,
|
|
90
|
-
0,
|
|
91
|
-
0,
|
|
92
|
-
0,
|
|
93
|
-
near * nf,
|
|
94
|
-
1,
|
|
95
|
-
]);
|
|
78
|
+
return new Float32Array([lr, 0, 0, 0, 0, bt, 0, 0, 0, 0, nf, 0, 0, 0, near * nf, 1]);
|
|
96
79
|
}
|
|
97
80
|
|
|
98
81
|
export function multiply(a: Float32Array, b: Float32Array): Float32Array {
|