WIP
This commit is contained in:
parent
34ac200437
commit
39b0160064
136 changed files with 7129 additions and 1950 deletions
|
|
@ -1,14 +1,24 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
import { appConfig } from '../../config';
|
||||
import { clamp } from '../../utils/clamp';
|
||||
import {
|
||||
createCachedFloat32BufferWrite,
|
||||
writeFloat32BufferIfChanged,
|
||||
} from '../../utils/graphics/cached-buffer-write';
|
||||
import { smartCompile } from '../../utils/graphics/smart-compile';
|
||||
import { CommonState } from '../common-state/common-state';
|
||||
import { BrushSettings } from './brush-settings';
|
||||
import shader from './brush.wgsl?raw';
|
||||
|
||||
interface LineSegment {
|
||||
from: vec2;
|
||||
to: vec2;
|
||||
}
|
||||
|
||||
export class BrushPipeline {
|
||||
private static readonly UNIFORM_COUNT = 2;
|
||||
private static readonly MAX_LINE_COUNT = 20;
|
||||
private static readonly UNIFORM_COUNT = 8;
|
||||
private static readonly MAX_LINE_COUNT = appConfig.pipelines.brush.maxLineCount;
|
||||
private static readonly VERTICES_PER_LINE_SEGMENT = 6;
|
||||
private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 6;
|
||||
|
||||
|
|
@ -16,10 +26,20 @@ export class BrushPipeline {
|
|||
private readonly bindGroup: GPUBindGroup;
|
||||
private readonly pipeline: GPURenderPipeline;
|
||||
private readonly uniforms: GPUBuffer;
|
||||
private readonly uniformValues = new Float32Array(BrushPipeline.UNIFORM_COUNT);
|
||||
private readonly uniformCache = createCachedFloat32BufferWrite(
|
||||
BrushPipeline.UNIFORM_COUNT
|
||||
);
|
||||
private readonly vertexBuffer: GPUBuffer;
|
||||
private readonly vertexUploadData = new Float32Array(
|
||||
BrushPipeline.MAX_LINE_COUNT *
|
||||
BrushPipeline.VERTICES_PER_LINE_SEGMENT *
|
||||
BrushPipeline.ATTRIBUTES_PER_LINE_SEGMENT
|
||||
);
|
||||
|
||||
private linePoints: Array<vec2> = [];
|
||||
private actualPoints: Array<vec2> = [];
|
||||
private lineSegments: Array<LineSegment> = [];
|
||||
private actualSegments: Array<LineSegment> = [];
|
||||
|
||||
public constructor(
|
||||
private readonly device: GPUDevice,
|
||||
|
|
@ -72,18 +92,6 @@ export class BrushPipeline {
|
|||
targets: [
|
||||
{
|
||||
format: 'rgba16float',
|
||||
blend: {
|
||||
color: {
|
||||
operation: 'add',
|
||||
srcFactor: 'zero',
|
||||
dstFactor: 'one',
|
||||
},
|
||||
alpha: {
|
||||
operation: 'max',
|
||||
srcFactor: 'one',
|
||||
dstFactor: 'one',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -111,112 +119,188 @@ export class BrushPipeline {
|
|||
}
|
||||
|
||||
public addSwipe(position: vec2) {
|
||||
this.linePoints.push(position);
|
||||
const previousPosition = this.linePoints[this.linePoints.length - 1] ?? position;
|
||||
this.addSwipeSegment(previousPosition, position);
|
||||
this.linePoints.push(vec2.clone(position));
|
||||
}
|
||||
|
||||
public addSwipeSegment(from: vec2, to: vec2) {
|
||||
this.lineSegments.push({
|
||||
from: vec2.clone(from),
|
||||
to: vec2.clone(to),
|
||||
});
|
||||
}
|
||||
|
||||
public clearSwipes() {
|
||||
this.linePoints.length = 0;
|
||||
this.lineSegments.length = 0;
|
||||
this.actualSegments.length = 0;
|
||||
}
|
||||
|
||||
public setParameters({ brushSize, brushSizeVariation }: BrushSettings) {
|
||||
this.device.queue.writeBuffer(
|
||||
public setParameters({
|
||||
brushSize,
|
||||
brushSizeVariation,
|
||||
selectedColorIndex,
|
||||
isErasing,
|
||||
}: BrushSettings & { selectedColorIndex: number; isErasing: boolean }) {
|
||||
this.uniformValues[0] = brushSize / 2;
|
||||
this.uniformValues[1] = Math.floor((brushSize / 2) * brushSizeVariation);
|
||||
this.uniformValues[2] = 0;
|
||||
this.uniformValues[3] = 0;
|
||||
this.uniformValues[4] = !isErasing && selectedColorIndex === 0 ? 1 : 0;
|
||||
this.uniformValues[5] = !isErasing && selectedColorIndex === 1 ? 1 : 0;
|
||||
this.uniformValues[6] = !isErasing && selectedColorIndex === 2 ? 1 : 0;
|
||||
this.uniformValues[7] = isErasing ? 0 : 1;
|
||||
writeFloat32BufferIfChanged(
|
||||
this.device,
|
||||
this.uniforms,
|
||||
0,
|
||||
new Float32Array([brushSize / 2, Math.floor((brushSize / 2) * brushSizeVariation)])
|
||||
this.uniformValues,
|
||||
this.uniformCache
|
||||
);
|
||||
|
||||
this.actualPoints = this.linePoints.slice();
|
||||
this.linePoints.splice(0, this.linePoints.length - 1);
|
||||
this.actualSegments = this.lineSegments.slice();
|
||||
this.lineSegments.length = 0;
|
||||
|
||||
if (this.actualPoints.length === 0) {
|
||||
if (this.actualSegments.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.actualPoints.length === 1) {
|
||||
this.actualPoints.push(this.actualPoints[0]); // allow single point swipes
|
||||
if (this.actualSegments.length > BrushPipeline.MAX_LINE_COUNT) {
|
||||
this.actualSegments = BrushPipeline.subsampleSegments(this.actualSegments);
|
||||
}
|
||||
|
||||
if (this.actualPoints.length > BrushPipeline.MAX_LINE_COUNT + 1) {
|
||||
this.actualPoints = BrushPipeline.subsampleLinePoints(this.actualPoints);
|
||||
const lineCount = this.lineCount;
|
||||
let floatOffset = 0;
|
||||
for (let i = 0; i < lineCount; i++) {
|
||||
const segment = this.actualSegments[i];
|
||||
floatOffset = this.writeSegmentVertices(
|
||||
this.vertexUploadData,
|
||||
floatOffset,
|
||||
segment.from,
|
||||
segment.to,
|
||||
brushSize / 2
|
||||
);
|
||||
}
|
||||
|
||||
this.device.queue.writeBuffer(
|
||||
this.vertexBuffer,
|
||||
0,
|
||||
new Float32Array(
|
||||
new Array(this.lineCount).fill(0).flatMap((_, i) => {
|
||||
const from = this.actualPoints[i];
|
||||
const to = this.actualPoints[i + 1];
|
||||
const [a, b, c, d] = this.getSegmentBoundingBox(from, to, brushSize / 2);
|
||||
return [a, b, c, b, c, d].flatMap((v) => [...v, ...from, ...to]);
|
||||
})
|
||||
)
|
||||
this.vertexUploadData,
|
||||
0,
|
||||
floatOffset
|
||||
);
|
||||
}
|
||||
|
||||
private static subsampleLinePoints(points: Array<vec2>): Array<vec2> {
|
||||
const lines = [];
|
||||
for (let i = 0; i < points.length - 2; i++) {
|
||||
lines.push({
|
||||
from: points[i],
|
||||
to: points[i + 1],
|
||||
length: vec2.dist(points[i], points[i + 1]),
|
||||
});
|
||||
private static subsampleSegments(segments: Array<LineSegment>): Array<LineSegment> {
|
||||
if (segments.length <= BrushPipeline.MAX_LINE_COUNT) {
|
||||
return segments;
|
||||
}
|
||||
|
||||
const sumLength = lines.reduce((sum, line) => sum + line.length, 0);
|
||||
|
||||
let currentLineIndex = 0;
|
||||
let lineLengthSoFar = 0;
|
||||
const result: Array<vec2> = [points[0]];
|
||||
for (let i = 1; i < BrushPipeline.MAX_LINE_COUNT; i++) {
|
||||
const t = (i * sumLength) / (BrushPipeline.MAX_LINE_COUNT + 1);
|
||||
while (lineLengthSoFar + lines[currentLineIndex].length < t) {
|
||||
lineLengthSoFar += lines[currentLineIndex].length;
|
||||
currentLineIndex++;
|
||||
}
|
||||
|
||||
const line = lines[currentLineIndex];
|
||||
const position = vec2.lerp(
|
||||
vec2.create(),
|
||||
line.from,
|
||||
line.to,
|
||||
(t - lineLengthSoFar) / line.length
|
||||
const result: Array<LineSegment> = [];
|
||||
for (let i = 0; i < BrushPipeline.MAX_LINE_COUNT; i++) {
|
||||
const index = Math.round(
|
||||
(i * (segments.length - 1)) / (BrushPipeline.MAX_LINE_COUNT - 1)
|
||||
);
|
||||
|
||||
result.push(position);
|
||||
result.push(segments[index]);
|
||||
}
|
||||
|
||||
result.push(points[points.length - 1]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getSegmentBoundingBox(from: vec2, to: vec2, width: number): Array<vec2> {
|
||||
let dir = vec2.sub(vec2.create(), to, from);
|
||||
vec2.normalize(dir, dir);
|
||||
private writeSegmentVertices(
|
||||
target: Float32Array,
|
||||
offset: number,
|
||||
from: vec2,
|
||||
to: vec2,
|
||||
width: number
|
||||
): number {
|
||||
const dx = to[0] - from[0];
|
||||
const dy = to[1] - from[1];
|
||||
const length = Math.hypot(dx, dy);
|
||||
const directionX = length > 0 ? dx / length : 1;
|
||||
const directionY = length > 0 ? dy / length : 0;
|
||||
const scaledDirectionX = directionX * width;
|
||||
const scaledDirectionY = directionY * width;
|
||||
const perpendicularX = directionY * width;
|
||||
const perpendicularY = -directionX * width;
|
||||
|
||||
if (vec2.len(dir) === 0) {
|
||||
dir = vec2.fromValues(1, 0); // allow single point swipes
|
||||
}
|
||||
const startX = from[0] - scaledDirectionX;
|
||||
const startY = from[1] - scaledDirectionY;
|
||||
const endX = to[0] + scaledDirectionX;
|
||||
const endY = to[1] + scaledDirectionY;
|
||||
|
||||
const perp = vec2.fromValues(dir[1], -dir[0]);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX + perpendicularX,
|
||||
startY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX - perpendicularX,
|
||||
startY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX + perpendicularX,
|
||||
endY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
startX - perpendicularX,
|
||||
startY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
offset = this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX + perpendicularX,
|
||||
endY + perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
return this.writeVertex(
|
||||
target,
|
||||
offset,
|
||||
endX - perpendicularX,
|
||||
endY - perpendicularY,
|
||||
from,
|
||||
to
|
||||
);
|
||||
}
|
||||
|
||||
vec2.scale(dir, dir, width);
|
||||
vec2.scale(perp, perp, width);
|
||||
|
||||
const offsetStart = vec2.sub(vec2.create(), from, dir);
|
||||
const offsetEnd = vec2.add(vec2.create(), to, dir);
|
||||
|
||||
return [
|
||||
vec2.add(vec2.create(), offsetStart, perp),
|
||||
vec2.sub(vec2.create(), offsetStart, perp),
|
||||
vec2.add(vec2.create(), offsetEnd, perp),
|
||||
vec2.sub(vec2.create(), offsetEnd, perp),
|
||||
];
|
||||
private writeVertex(
|
||||
target: Float32Array,
|
||||
offset: number,
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
from: vec2,
|
||||
to: vec2
|
||||
): number {
|
||||
target[offset++] = screenX;
|
||||
target[offset++] = screenY;
|
||||
target[offset++] = from[0];
|
||||
target[offset++] = from[1];
|
||||
target[offset++] = to[0];
|
||||
target[offset++] = to[1];
|
||||
return offset;
|
||||
}
|
||||
|
||||
public execute(commandEncoder: GPUCommandEncoder, trailMapOut: GPUTextureView) {
|
||||
if (this.lineCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderPassDescriptor: GPURenderPassDescriptor = {
|
||||
colorAttachments: [
|
||||
{
|
||||
|
|
@ -256,6 +340,6 @@ export class BrushPipeline {
|
|||
}
|
||||
|
||||
private get lineCount() {
|
||||
return clamp(this.actualPoints.length - 1, 0, BrushPipeline.MAX_LINE_COUNT);
|
||||
return clamp(this.actualSegments.length, 0, BrushPipeline.MAX_LINE_COUNT);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue