diff --git a/apps/typegpu-docs/src/examples/geometry/circles/index.ts b/apps/typegpu-docs/src/examples/geometry/circles/index.ts index 4061b2fff3..4ffd60441d 100644 --- a/apps/typegpu-docs/src/examples/geometry/circles/index.ts +++ b/apps/typegpu-docs/src/examples/geometry/circles/index.ts @@ -32,7 +32,6 @@ context.configure({ alphaMode: 'premultiplied', }); -// Textures let msaaTexture: GPUTexture; let msaaTextureView: GPUTextureView; @@ -137,7 +136,7 @@ const pipeline = root['~unstable'] setTimeout(() => { pipeline - .with(bindGroupLayout, uniformsBindGroup) + .with(uniformsBindGroup) .withColorAttachment({ ...(multisample ? { diff --git a/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts b/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts index 05a5fb8ae7..bfa69763c1 100644 --- a/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts +++ b/apps/typegpu-docs/src/examples/geometry/lines-combinations/index.ts @@ -1,20 +1,13 @@ import { - addMul, + caps, endCapSlot, + joins, joinSlot, - lineCaps, - lineJoins, - lineSegmentIndicesCapLevel0, - lineSegmentIndicesCapLevel1, - lineSegmentIndicesCapLevel2, - lineSegmentIndicesCapLevel3, + lineSegmentIndices, + lineSegmentLeftIndices, lineSegmentVariableWidth, - lineSegmentWireframeIndicesCapLevel0, - lineSegmentWireframeIndicesCapLevel1, - lineSegmentWireframeIndicesCapLevel2, - lineSegmentWireframeIndicesCapLevel3, + lineSegmentWireframeIndices, startCapSlot, - uvToLineSegment, } from '@typegpu/geometry'; import tgpu from 'typegpu'; import { @@ -29,7 +22,18 @@ import { vec3f, vec4f, } from 'typegpu/data'; -import { clamp, cos, min, mix, select, sin } from 'typegpu/std'; +import { + add, + clamp, + cos, + fwidth, + min, + mix, + mul, + select, + sin, + smoothstep, +} from 'typegpu/std'; import type { ColorAttachment } from '../../../../../../packages/typegpu/src/core/pipeline/renderPipeline.ts'; import { TEST_SEGMENT_COUNT } from './constants.ts'; import * as testCases from './testCases.ts'; @@ -62,6 +66,26 @@ context.configure({ alphaMode: 'premultiplied', }); +let msaaTexture: GPUTexture; +let msaaTextureView: GPUTextureView; + +const createDepthAndMsaaTextures = () => { + if (msaaTexture) { + msaaTexture.destroy(); + } + msaaTexture = device.createTexture({ + size: [canvas.width, canvas.height, 1], + format: presentationFormat, + sampleCount: 4, + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + msaaTextureView = msaaTexture.createView(); +}; + +createDepthAndMsaaTextures(); +const resizeObserver = new ResizeObserver(createDepthAndMsaaTextures); +resizeObserver.observe(canvas); + const Uniforms = struct({ time: f32, fillType: u32, @@ -84,17 +108,29 @@ const uniformsBindGroup = root.createBindGroup(bindGroupLayout, { uniforms: uniformsBuffer, }); +const MAX_JOIN_COUNT = 6; +const indices = lineSegmentIndices(MAX_JOIN_COUNT); +const indicesLeft = lineSegmentLeftIndices(MAX_JOIN_COUNT); +const wireframeIndices = lineSegmentWireframeIndices(MAX_JOIN_COUNT); + const indexBuffer = root .createBuffer( - arrayOf(u16, lineSegmentIndicesCapLevel3.length), - lineSegmentIndicesCapLevel3, + arrayOf(u16, indices.length), + indices, + ) + .$usage('index'); + +const indexBufferLeft = root + .createBuffer( + arrayOf(u16, indicesLeft.length), + indicesLeft, ) .$usage('index'); const outlineIndexBuffer = root .createBuffer( - arrayOf(u16, lineSegmentWireframeIndicesCapLevel3.length), - lineSegmentWireframeIndicesCapLevel3, + arrayOf(u16, wireframeIndices.length), + wireframeIndices, ) .$usage('index'); @@ -133,16 +169,22 @@ const mainVertex = tgpu['~unstable'].vertexFn({ }; } - const result = lineSegmentVariableWidth(vertexIndex, A, B, C, D); - const uv = uvToLineSegment(B.position, C.position, result.vertexPosition); + const result = lineSegmentVariableWidth( + vertexIndex, + A, + B, + C, + D, + MAX_JOIN_COUNT, + ); return { - outPos: vec4f(result.vertexPosition, 0, 1), + outPos: vec4f(mul(result.vertexPosition, result.w), 0, result.w), position: result.vertexPosition, - uv: uv, + uv: vec2f(0, select(f32(0), f32(1), vertexIndex > 1)), instanceIndex, vertexIndex, - situationIndex: result.situationIndex, + situationIndex: 0, }; }); @@ -171,14 +213,6 @@ const mainFragment = tgpu['~unstable'].fragmentFn({ }) => { 'use gpu'; const fillType = bindGroupLayout.$.uniforms.fillType; - if (fillType === 1) { - // typegpu gradient - return mix( - vec4f(0.77, 0.39, 1, 0.5), - vec4f(0.11, 0.44, 0.94, 0.5), - position.x * 0.5 + 0.5, - ); - } let color = vec3f(); const colors = [ vec3f(1, 0, 0), // 0 @@ -191,18 +225,33 @@ const mainFragment = tgpu['~unstable'].fragmentFn({ vec3f(0.25, 0.75, 0.25), // 7 vec3f(0.25, 0.25, 0.75), // 8 ]; + if (fillType === 1) { + // typegpu gradient + color = mix( + vec3f(0.77, 0.39, 1), + vec3f(0.11, 0.44, 0.94), + position.x * 0.5 + 0.5, + ); + } if (fillType === 2) { - color = colors[instanceIndex % colors.length]; + let t = cos(uv.y * 10); + t = clamp(t / fwidth(t), 0, 1); + color = mix( + vec3f(0.77, 0.39, 1), + vec3f(0.11, 0.44, 0.94), + t, + ); } if (fillType === 3) { - color = colors[vertexIndex % colors.length]; + color = vec3f(colors[vertexIndex % colors.length]); } if (fillType === 4) { - color = colors[situationIndex % colors.length]; + color = vec3f(colors[instanceIndex % colors.length]); } if (fillType === 5) { - color = vec3f(uv.x, cos(uv.y * 100), 0); + color = vec3f(colors[situationIndex % colors.length]); } + color = mul(color, 0.8 + 0.2 * smoothstep(1, 0.5, uv.y)); if (frontFacing) { return vec4f(color, 0.5); } @@ -284,14 +333,14 @@ const circlesVertex = tgpu['~unstable'].vertexFn({ const angle = min(2 * Math.PI, step * f32(vertexIndex)); const unit = vec2f(cos(angle), sin(angle)); return { - outPos: vec4f(addMul(vertex.position, unit, vertex.radius), 0, 1), + outPos: vec4f(add(vertex.position, mul(unit, vertex.radius)), 0, 1), }; }); let testCase = testCases.arms; -let join = lineJoins.round; -let startCap = lineCaps.round; -let endCap = lineCaps.round; +let join = joins.round; +let startCap = caps.round; +let endCap = caps.round; function createPipelines() { const fill = root['~unstable'] @@ -307,8 +356,9 @@ function createPipelines() { .withPrimitive({ // cullMode: 'back', }) + .withMultisample({ count: multisample ? 4 : 1 }) .createPipeline() - .withIndexBuffer(indexBuffer); + .withIndexBuffer(oneSided ? indexBufferLeft : indexBuffer); const outline = root['~unstable'] .with(joinSlot, join) @@ -323,6 +373,7 @@ function createPipelines() { .withPrimitive({ topology: 'line-list', }) + .withMultisample({ count: multisample ? 4 : 1 }) .createPipeline() .withIndexBuffer(outlineIndexBuffer); @@ -336,6 +387,7 @@ function createPipelines() { .withPrimitive({ topology: 'line-strip', }) + .withMultisample({ count: multisample ? 4 : 1 }) .createPipeline(); const circles = root['~unstable'] @@ -348,6 +400,7 @@ function createPipelines() { .withPrimitive({ topology: 'line-list', }) + .withMultisample({ count: multisample ? 4 : 1 }) .createPipeline(); return { @@ -358,24 +411,27 @@ function createPipelines() { }; } -let pipelines = createPipelines(); - +let multisample = true; let showRadii = false; -let wireframe = true; +let wireframe = false; +let oneSided = false; let fillType = 1; let animationSpeed = 1; let reverse = false; -let subdiv = { - fillCount: lineSegmentIndicesCapLevel3.length, - wireframeCount: lineSegmentWireframeIndicesCapLevel3.length, -}; + +let pipelines = createPipelines(); const draw = (timeMs: number) => { uniformsBuffer.writePartial({ time: timeMs * 1e-3, }); const colorAttachment: ColorAttachment = { - view: context.getCurrentTexture().createView(), + view: multisample + ? msaaTextureView + : context.getCurrentTexture().createView(), + resolveTarget: multisample + ? context.getCurrentTexture().createView() + : undefined, clearValue: [1, 1, 1, 1], loadOp: 'load', storeOp: 'store', @@ -388,13 +444,16 @@ const draw = (timeMs: number) => { console.log(`${(Number(end - start) * 1e-6).toFixed(2)} ms`); } }) - .drawIndexed(subdiv.fillCount, fillType === 0 ? 0 : TEST_SEGMENT_COUNT); + .drawIndexed( + oneSided ? indicesLeft.length : indices.length, + fillType === 0 ? 0 : TEST_SEGMENT_COUNT, + ); if (wireframe) { pipelines.outline .with(uniformsBindGroup) .withColorAttachment(colorAttachment) - .drawIndexed(subdiv.wireframeCount, TEST_SEGMENT_COUNT); + .drawIndexed(wireframeIndices.length, TEST_SEGMENT_COUNT); } if (showRadii) { pipelines.circles @@ -424,32 +483,20 @@ runAnimationFrame(0); const fillOptions = { none: 0, solid: 1, - instance: 2, + distanceToSegment: 2, triangle: 3, - situation: 4, - distanceToSegment: 5, + instance: 4, + // situation: 5, }; -const subdivs = [ - { - fillCount: lineSegmentIndicesCapLevel0.length, - wireframeCount: lineSegmentWireframeIndicesCapLevel0.length, - }, - { - fillCount: lineSegmentIndicesCapLevel1.length, - wireframeCount: lineSegmentWireframeIndicesCapLevel1.length, - }, - { - fillCount: lineSegmentIndicesCapLevel2.length, - wireframeCount: lineSegmentWireframeIndicesCapLevel2.length, - }, - { - fillCount: lineSegmentIndicesCapLevel3.length, - wireframeCount: lineSegmentWireframeIndicesCapLevel3.length, - }, -]; - export const controls = { + 'MSAA x4': { + initial: multisample, + onToggleChange: (value: boolean) => { + multisample = value; + pipelines = createPipelines(); + }, + }, 'Test Case': { initial: Object.keys(testCases)[0], options: Object.keys(testCases), @@ -460,25 +507,25 @@ export const controls = { }, 'Start Cap': { initial: 'round', - options: Object.keys(lineCaps), - onSelectChange: async (selected: keyof typeof lineCaps) => { - startCap = lineCaps[selected]; + options: Object.keys(caps), + onSelectChange: async (selected: keyof typeof caps) => { + startCap = caps[selected]; pipelines = createPipelines(); }, }, 'End Cap': { initial: 'round', - options: Object.keys(lineCaps), - onSelectChange: async (selected: keyof typeof lineCaps) => { - endCap = lineCaps[selected]; + options: Object.keys(caps), + onSelectChange: async (selected: keyof typeof caps) => { + endCap = caps[selected]; pipelines = createPipelines(); }, }, Join: { initial: 'round', - options: Object.keys(lineJoins), - onSelectChange: async (selected: keyof typeof lineJoins) => { - join = lineJoins[selected]; + options: Object.keys(joins), + onSelectChange: async (selected: keyof typeof joins) => { + join = joins[selected]; pipelines = createPipelines(); }, }, @@ -490,29 +537,27 @@ export const controls = { uniformsBuffer.writePartial({ fillType }); }, }, - 'Subdiv. Level': { - initial: 2, - min: 0, - step: 1, - max: 3, - onSliderChange: (value: number) => { - subdiv = subdivs[value]; - }, - }, Wireframe: { - initial: true, + initial: wireframe, onToggleChange: (value: boolean) => { wireframe = value; }, }, + 'One sided': { + initial: oneSided, + onToggleChange: (value: boolean) => { + oneSided = value; + pipelines = createPipelines(); + }, + }, 'Radius and centerline': { - initial: false, + initial: showRadii, onToggleChange: (value: boolean) => { showRadii = value; }, }, 'Animation speed': { - initial: 1, + initial: animationSpeed, min: 0, step: 0.001, max: 5, @@ -521,7 +566,7 @@ export const controls = { }, }, Reverse: { - initial: false, + initial: reverse, onToggleChange: (value: boolean) => { reverse = value; }, diff --git a/apps/typegpu-docs/src/examples/geometry/lines-combinations/testCases.ts b/apps/typegpu-docs/src/examples/geometry/lines-combinations/testCases.ts index 05f9f63512..b29a610099 100644 --- a/apps/typegpu-docs/src/examples/geometry/lines-combinations/testCases.ts +++ b/apps/typegpu-docs/src/examples/geometry/lines-combinations/testCases.ts @@ -1,4 +1,4 @@ -import { LineSegmentVertex } from '@typegpu/geometry'; +import { LineControlPoint } from '@typegpu/geometry'; import { perlin2d, randf } from '@typegpu/noise'; import tgpu from 'typegpu'; import { arrayOf, f32, i32, mat2x2f, u32, vec2f } from 'typegpu/data'; @@ -8,6 +8,7 @@ import { clamp, cos, floor, + max, mul, pow, select, @@ -15,253 +16,227 @@ import { } from 'typegpu/std'; import { TEST_SEGMENT_COUNT } from './constants.ts'; -const testCaseShell = tgpu.fn([u32, f32], LineSegmentVertex); +const testCaseShell = tgpu.fn([u32, f32], LineControlPoint); const segmentSide = tgpu.const(arrayOf(f32, 4), [-1, -1, 1, 1]); -export const segmentAlternate = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const side = segmentSide.$[vertexIndex]; - const r = sin(time + select(0, Math.PI / 2, side === -1)); - const radius = 0.4 * r * r; - return LineSegmentVertex({ - position: vec2f(0.5 * side * cos(time), 0.5 * side * sin(time)), - radius, - }); - }, -); - -export const segmentStretch = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const side = segmentSide.$[vertexIndex]; - const distance = 0.5 * clamp(0.55 * sin(1.5 * time) + 0.5, 0, 1); - return LineSegmentVertex({ - position: vec2f(distance * side * cos(time), distance * side * sin(time)), - radius: 0.25, - }); - }, -); - -export const segmentContainsAnotherEnd = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const side = segmentSide.$[vertexIndex]; - return LineSegmentVertex({ - position: vec2f(side * 0.25 * (1 + clamp(sin(time), -0.8, 1)), 0), - radius: 0.25 + side * 0.125, - }); - }, -); - -export const caseVShapeSmall = testCaseShell( - (vertexIndex, t) => { - 'use gpu'; - const side = clamp(f32(vertexIndex) - 2, -1, 1); - const isMiddle = side === 0; - return LineSegmentVertex({ - position: vec2f(0.5 * side, select(0.5 * cos(t), 0, isMiddle)), - radius: select(0.1, 0.2, isMiddle), - }); - }, -); - -export const caseVShapeBig = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const side = clamp(f32(vertexIndex) - 2, -1, 1); - const isMiddle = side === 0; - return LineSegmentVertex({ - position: vec2f(0.5 * side, select(0.5 * cos(time), 0, isMiddle)), - radius: select(0.3, 0.2, isMiddle), - }); - }, -); - -export const halfCircle = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const angle = Math.PI * clamp(f32(vertexIndex) - 1, 0, 50) / 50; - const radius = 0.5 * cos(time); - return LineSegmentVertex({ - position: vec2f(radius * cos(angle), radius * sin(angle)), - radius: 0.2, - }); - }, -); - -export const halfCircleThin = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const result = halfCircle(vertexIndex, time); - result.radius = 0.01; - return result; - }, -); - -export const bending = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const i = clamp(f32(vertexIndex) - 1, 0, 48) / 48; - const x = 2 * i - 1; - const s = sin(time); - const n = 10 * s * s * s * s + 0.25; - const base = clamp(1 - pow(abs(x), n), 0, 1); - return LineSegmentVertex({ - position: vec2f(0.5 * x, 0.5 * pow(base, 1 / n)), - radius: 0.2, - }); - }, -); - -export const animateWidth = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const i = (f32(vertexIndex) % TEST_SEGMENT_COUNT) / TEST_SEGMENT_COUNT; - const x = cos(4 * 2 * Math.PI * i + Math.PI / 2); - const y = cos(5 * 2 * Math.PI * i); - return LineSegmentVertex({ - position: vec2f(0.8 * x, 0.8 * y), - radius: 0.05 * clamp(sin(8 * Math.PI * i - 3 * time), 0.1, 1), - }); - }, -); - -export const perlinTraces = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const perLine = u32(200); - const n = floor(f32(vertexIndex) / f32(perLine)); - const x = - 2 * f32(clamp(vertexIndex % perLine, 2, perLine - 2)) / f32(perLine) - 1; - const value = - 0.5 * perlin2d.sample(vec2f(2 * x + 2 * time, time + 0.1 * n)) + - 0.25 * perlin2d.sample(vec2f(4 * x, time + 100 + 0.1 * n)) + - 0.125 * perlin2d.sample(vec2f(8 * x, time + 200 + 0.2 * n)) + - 0.0625 * perlin2d.sample(vec2f(16 * x, time + 300 + 0.3 * n)); - const y = 0.125 * n - 0.5 + 0.5 * value; - const radiusFactor = 0.025 * (n + 1); - return LineSegmentVertex({ - position: vec2f(0.8 * x, y), - radius: select( - radiusFactor * radiusFactor, - -1, - vertexIndex % perLine === 0, - ), - }); - }, -); - -export const bars = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const VERTS_PER_LINE = u32(5); - const lineIndex = f32(u32(vertexIndex / VERTS_PER_LINE)); - const y = f32(clamp(vertexIndex % VERTS_PER_LINE, 1, 2) - 1); - const x = 20 * - (2 * f32(VERTS_PER_LINE) * lineIndex / TEST_SEGMENT_COUNT - 1); - return LineSegmentVertex({ - position: vec2f(0.8 * x, 0.8 * y * sin(x + time)), - radius: select( - clamp(0.08 * abs(sin(x + time)), 0, 0.01), - -1, - vertexIndex % 5 === 4, - ), - }); - }, -); - -export const arms = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const s = sin(time); - const c = cos(time); - const r = 0.25; - const points = [ - vec2f(r * s - 0.25, r * c), - vec2f(-0.25, 0), - vec2f(0.25, 0), - vec2f(-r * s + 0.25, r * c), - ]; - const i = clamp(i32(vertexIndex) - 1, 0, 3); - return LineSegmentVertex({ - position: points[i], - radius: 0.2, - }); - }, -); - -export const armsSmall = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const result = arms(vertexIndex, time); - return LineSegmentVertex({ - position: result.position, - radius: select(0.1, 0.2, vertexIndex === 2 || vertexIndex === 3), - }); - }, -); - -export const armsBig = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const result = arms(vertexIndex, time); - return LineSegmentVertex({ - position: result.position, - radius: select(0.275, 0.1, vertexIndex === 2 || vertexIndex === 3), - }); - }, -); - -export const armsRotating = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const s = sin(time); - const c = cos(time); - const r = 0.25; - const points = [ - vec2f(r * s - 0.25, r * c), - vec2f(-0.25, 0), - vec2f(0.25, 0), - vec2f(-r * s + 0.25, -r * c), - ]; - const i = clamp(i32(vertexIndex) - 1, 0, 3); - return LineSegmentVertex({ - position: points[i], - radius: 0.2, - }); - }, -); - -export const flyingSquares = testCaseShell( - (vertexIndex, time) => { - 'use gpu'; - const squareIndex = u32(vertexIndex / 8); - randf.seed(f32(squareIndex + 5)); - const squarePoints = [ - vec2f(-1, -1), - vec2f(1, -1), - vec2f(1, 1), - vec2f(-1, 1), - ]; - const pointIndex = vertexIndex % 8; - const point = squarePoints[pointIndex % 4]; - const rotationSpeed = 2 * randf.sample() - 1; - const s = sin(time * rotationSpeed); - const c = cos(time * rotationSpeed); - const rotate = mat2x2f(c, -s, s, c); - const r = 0.1 + 0.05 * randf.sample(); - const x = 2.0 * randf.sample() - 1; - const y = 2.0 * randf.sample() - 1; - const transformedPoint = add(vec2f(x, y), mul(rotate, mul(point, r))); - return LineSegmentVertex({ - position: transformedPoint, - radius: select( - 0.1 * r + 0.05 * randf.sample(), - -1, - pointIndex === 7 || squareIndex > 50, - ), - }); - }, -); +export const segmentAlternate = testCaseShell((vertexIndex, time) => { + 'use gpu'; + let side = segmentSide.$[vertexIndex]; + const r = sin(time + select(0, Math.PI / 2, side === -1)); + const radius = 0.4 * r * r; + return LineControlPoint({ + position: vec2f(0.5 * side * cos(time), 0.5 * side * sin(time)), + radius, + }); +}); + +export const segmentStretch = testCaseShell((vertexIndex, time) => { + 'use gpu'; + let side = segmentSide.$[vertexIndex]; + const distance = 0.5 * clamp(0.55 * sin(1.5 * time) + 0.5, 0, 1); + return LineControlPoint({ + position: vec2f(distance * side * cos(time), distance * side * sin(time)), + radius: 0.25, + }); +}); + +export const segmentContainsAnotherEnd = testCaseShell((vertexIndex, time) => { + 'use gpu'; + let side = segmentSide.$[vertexIndex]; + return LineControlPoint({ + position: vec2f(side * 0.25 * (1 + clamp(sin(time), -0.8, 1)), 0), + radius: 0.25 + side * 0.125, + }); +}); + +export const caseVShapeSmall = testCaseShell((vertexIndex, t) => { + 'use gpu'; + const side = clamp(f32(vertexIndex) - 2, -1, 1); + const isMiddle = side === 0; + return LineControlPoint({ + position: vec2f(0.5 * side, select(0.5 * cos(t), 0, isMiddle)), + radius: select(0.1, 0.2, isMiddle), + }); +}); + +export const caseVShapeBig = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const side = clamp(f32(vertexIndex) - 2, -1, 1); + const isMiddle = side === 0; + return LineControlPoint({ + position: vec2f(0.5 * side, select(0.5 * cos(time), 0, isMiddle)), + radius: select(0.3, 0.2, isMiddle), + }); +}); + +export const halfCircle = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const angle = (Math.PI * clamp(f32(vertexIndex) - 1, 0, 50)) / 50; + const radius = 0.5 * cos(time); + return LineControlPoint({ + position: vec2f(radius * cos(angle), radius * sin(angle)), + radius: 0.2, + }); +}); + +export const halfCircleThin = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const result = halfCircle(vertexIndex, time); + result.radius = 0.01; + return result; +}); + +export const bending = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const i = clamp(f32(vertexIndex) - 1, 0, 48) / 48; + const x = 2 * i - 1; + const s = sin(time); + const n = 10 * s * s * s * s + 0.25; + const base = clamp(1 - pow(abs(x), n), 0, 1); + return LineControlPoint({ + position: vec2f(0.5 * x, 0.5 * pow(base, 1 / n)), + radius: 0.2, + }); +}); + +export const animateWidth = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const i = (f32(vertexIndex) % TEST_SEGMENT_COUNT) / TEST_SEGMENT_COUNT; + const x = cos(4 * 2 * Math.PI * i + Math.PI / 2); + const y = cos(5 * 2 * Math.PI * i); + return LineControlPoint({ + position: vec2f(0.8 * x, 0.8 * y), + radius: 0.05 * clamp(sin(8 * Math.PI * i - 3 * time), 0.1, 1), + }); +}); + +export const perlinTraces = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const perLine = u32(200); + const n = floor(f32(vertexIndex) / f32(perLine)); + const x = + (2 * f32(clamp(vertexIndex % perLine, 2, perLine - 2))) / f32(perLine) - 1; + const value = 0.5 * perlin2d.sample(vec2f(2 * x + 2 * time, time + 0.1 * n)) + + 0.25 * perlin2d.sample(vec2f(4 * x, time + 100 + 0.1 * n)) + + 0.125 * perlin2d.sample(vec2f(8 * x, time + 200 + 0.2 * n)) + + 0.0625 * perlin2d.sample(vec2f(16 * x, time + 300 + 0.3 * n)); + const y = 0.15 * n - 0.75 + 0.5 * value; + const radiusFactor = 0.025 * (n + 1); + return LineControlPoint({ + position: vec2f(0.8 * x, y), + radius: select( + radiusFactor * radiusFactor, + -1, + vertexIndex % perLine === 0, + ), + }); +}); + +export const bars = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const VERTS_PER_LINE = u32(5); + const lineIndex = f32(u32(vertexIndex / VERTS_PER_LINE)); + const y = f32(clamp(vertexIndex % VERTS_PER_LINE, 1, 2) - 1); + const x = 20 * + ((2 * f32(VERTS_PER_LINE) * lineIndex) / TEST_SEGMENT_COUNT - 1); + return LineControlPoint({ + position: vec2f(0.8 * x, 0.8 * y * sin(x + time)), + radius: select( + clamp(0.08 * abs(sin(x + time)), 0, 0.01), + -1, + vertexIndex % 5 === 4, + ), + }); +}); + +export const arms = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const s = sin(time); + const c = cos(time); + const r = 0.25; + const points = [ + vec2f(r * s - 0.25, r * c), + vec2f(-0.25, 0), + vec2f(0.25, 0), + vec2f(-r * s + 0.25, r * c), + ]; + const i = clamp(i32(vertexIndex) - 1, 0, 3); + return LineControlPoint({ + position: points[i], + radius: 0.2, + }); +}); + +export const armsSmall = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const result = arms(vertexIndex, time); + return LineControlPoint({ + position: result.position, + radius: select(0.1, 0.2, vertexIndex === 2 || vertexIndex === 3), + }); +}); + +export const armsBig = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const result = arms(vertexIndex, time); + return LineControlPoint({ + position: result.position, + radius: select(0.2, 0.1, vertexIndex === 2 || vertexIndex === 3), + }); +}); + +export const armsRotating = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const s = sin(time); + const c = cos(time); + const r = 0.25; + const points = [ + vec2f(r * s - 0.25, r * c), + vec2f(-0.25, 0), + vec2f(0.25, 0), + vec2f(-r * s + 0.25, -r * c), + ]; + const i = clamp(i32(vertexIndex) - 1, 0, 3); + return LineControlPoint({ + position: points[i], + radius: 0.2, + }); +}); + +export const flyingSquares = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const squareIndex = u32(vertexIndex / 8); + randf.seed(f32(squareIndex + 5)); + const squarePoints = [vec2f(-1, -1), vec2f(1, -1), vec2f(1, 1), vec2f(-1, 1)]; + const pointIndex = vertexIndex % 8; + const point = squarePoints[pointIndex % 4]; + const rotationSpeed = 2 * randf.sample() - 1; + const s = sin(time * rotationSpeed); + const c = cos(time * rotationSpeed); + const rotate = mat2x2f(c, -s, s, c); + const r = 0.1 + 0.05 * randf.sample(); + const x = 2.0 * randf.sample() - 1; + const y = 2.0 * randf.sample() - 1; + const transformedPoint = add(vec2f(x, y), mul(rotate, mul(point, r))); + return LineControlPoint({ + position: transformedPoint, + radius: select( + 0.1 * r + 0.05 * randf.sample(), + -1, + pointIndex === 7 || squareIndex > 50, + ), + }); +}); + +export const spring = testCaseShell((vertexIndex, time) => { + 'use gpu'; + const i = clamp(i32(vertexIndex - 1), 0, 20); + return LineControlPoint({ + position: vec2f( + f32(max(0, i - 1)) * (0.1 + 0.09 * sin(time)) - 0.9, + select(-0.25, 0.25, (i & 0b1) === 0), + ), + radius: 0.05, + }); +}); diff --git a/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts b/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts index 9a7e8f1217..3ef76d8936 100644 --- a/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts +++ b/apps/typegpu-docs/src/examples/simulation/wind-map/index.ts @@ -1,9 +1,9 @@ import { + caps, endCapSlot, - joinSlot, - lineSegmentIndicesCapLevel1, + LineControlPoint, + lineSegmentIndices, lineSegmentVariableWidth, - LineSegmentVertex, startCapSlot, } from '@typegpu/geometry'; import tgpu from 'typegpu'; @@ -18,7 +18,6 @@ import { vec2f, vec4f, } from 'typegpu/data'; -import { lineCaps, lineJoins } from '@typegpu/geometry'; import { add, clamp, mix, mul, normalize, select } from 'typegpu/std'; const root = await tgpu.init({ @@ -95,9 +94,11 @@ const bindGroupWritable = root.createBindGroup(bindGroupLayoutWritable, { particles: particleTrailsBuffer, }); +const MAX_JOIN_COUNT = 3; +const indices = lineSegmentIndices(MAX_JOIN_COUNT); const indexBuffer = root.createBuffer( - arrayOf(u16, lineSegmentIndicesCapLevel1.length), - lineSegmentIndicesCapLevel1, + arrayOf(u16, indices.length), + indices, ).$usage('index'); // const vectorField = tgpu.fn([vec2f], vec2f)((pos) => { @@ -123,8 +124,8 @@ const advectCompute = tgpu['~unstable'].computeFn({ const v0 = vectorField(pos); const v1 = vectorField(add(pos, mul(v0, 0.5 * stepSize))); const newPos = add(pos, mul(v1, stepSize)); - particle.positions[currentPosIndex] = newPos; - bindGroupLayoutWritable.$.particles[particleIndex] = particle; + particle.positions[currentPosIndex] = vec2f(newPos); + bindGroupLayoutWritable.$.particles[particleIndex] = ParticleTrail(particle); }); const lineWidth = tgpu.fn([f32], f32)((x) => 0.004 * (1 - x)); @@ -165,24 +166,31 @@ const mainVertex = tgpu['~unstable'].vertexFn({ const iB = trailIndex; const iC = (TRAIL_LENGTH + trailIndex - 1) % TRAIL_LENGTH; const iD = (TRAIL_LENGTH + trailIndex - 2) % TRAIL_LENGTH; - const A = LineSegmentVertex({ + const A = LineControlPoint({ position: particle.positions[iA], radius: lineWidth(f32(trailIndexOriginal) / (TRAIL_LENGTH - 1)), }); - const B = LineSegmentVertex({ + const B = LineControlPoint({ position: particle.positions[iB], radius: lineWidth(f32(trailIndexOriginal + 1) / (TRAIL_LENGTH - 1)), }); - const C = LineSegmentVertex({ + const C = LineControlPoint({ position: particle.positions[iC], radius: lineWidth(f32(trailIndexOriginal + 2) / (TRAIL_LENGTH - 1)), }); - const D = LineSegmentVertex({ + const D = LineControlPoint({ position: particle.positions[iD], radius: lineWidth(f32(trailIndexOriginal + 3) / (TRAIL_LENGTH - 1)), }); - const result = lineSegmentVariableWidth(vertexIndex, A, B, C, D); + const result = lineSegmentVariableWidth( + vertexIndex, + A, + B, + C, + D, + MAX_JOIN_COUNT, + ); return { outPos: vec4f(result.vertexPosition, 0, 1), @@ -223,9 +231,8 @@ function createPipelines() { .createPipeline(); const fill = root['~unstable'] - .with(joinSlot, lineJoins.round) - .with(startCapSlot, lineCaps.arrow) - .with(endCapSlot, lineCaps.butt) + .with(startCapSlot, caps.arrow) + .with(endCapSlot, caps.butt) .withVertex(mainVertex, {}) .withFragment(mainFragment, { format: presentationFormat, @@ -246,13 +253,13 @@ const draw = () => { uniformsBuffer.writePartial({ frameCount }); pipelines.advect - .with(bindGroupLayoutWritable, bindGroupWritable) + .with(bindGroupWritable) .dispatchWorkgroups( Math.ceil(PARTICLE_COUNT / WORKGROUP_SIZE), ); pipelines.fill - .with(bindGroupLayout, bindGroup) + .with(bindGroup) .withColorAttachment({ view: context.getCurrentTexture().createView(), clearValue: [1, 1, 1, 1], @@ -260,7 +267,7 @@ const draw = () => { storeOp: 'store', }) .drawIndexed( - lineSegmentIndicesCapLevel1.length, + indices.length, PARTICLE_COUNT * TRAIL_LENGTH, ); }; diff --git a/packages/typegpu-geometry/LinesExplanation.md b/packages/typegpu-geometry/LinesExplanation.md new file mode 100644 index 0000000000..706b8600bd --- /dev/null +++ b/packages/typegpu-geometry/LinesExplanation.md @@ -0,0 +1,72 @@ +This is a plan for a future article. + +## Sources + +- https://mattdesl.svbtle.com/drawing-lines-is-hard +- https://blog.mapbox.com/drawing-antialiased-lines-with-opengl-8766f34192dc +- https://wwwtyro.net/2019/11/18/instanced-lines.html +- https://wwwtyro.net/2021/10/01/instanced-lines-part-2.html + +## Goals + +- variable width line +- choice of start / end caps, joins +- minimal overlaps +- visually acceptable behavior in extreme cases +- single draw call (easy to use) +- easily colored based on contour level sets +- easily colored based on distance along line (dashes) +- minimize complexity +- single-sided expansion for non-overlapping outlines + +## Non-goals + +- maximum performance +- triangle counts +- overdraw (having max-area triangles) + +## Basics + +- start with variable width line segment computations +- caps +- joining segments +- triangulation + +## Basic example + +- implement a simple render command which renders a line of segments which + change width based on where the mouse cursor is, with a nice shading + +## Overlaps + +- explain the problem +- reverse miter solution +- intersecting reverse miters + +## Edge cases + +- enumerate situations a join can be in +- detecting whether to join or reverse miter +- collapsing joins into miters +- hairpin detection and what is done in this case +- degenerate lines (start point "inside" end point) + +## Example showing off reverse miter + +- tbd + +## Shading + +- naive approach and the shortcomings +- homogeneous coordinates and "perspective" correction +- contour level sets +- constant width outline +- dashes + +## Single-sided expansion + +- thick outline / box shadow example + +## Miter join math + +## Arrow cap math diff --git a/packages/typegpu-geometry/src/index.ts b/packages/typegpu-geometry/src/index.ts index 0ffe08fe16..0bfbe1ebc2 100644 --- a/packages/typegpu-geometry/src/index.ts +++ b/packages/typegpu-geometry/src/index.ts @@ -1,8 +1,3 @@ export * from './circle.ts'; -export * from './lines/caps/index.ts'; -export * from './lines/indices.ts'; -export * from './lines/joins/index.ts'; -export * from './lines/lines.ts'; -export * from './lines/types.ts'; -export { uvToLineSegment } from './lines/utils.ts'; +export * from './lines/index.ts'; export { addMul } from './utils.ts'; diff --git a/packages/typegpu-geometry/src/lines/caps/arrow.ts b/packages/typegpu-geometry/src/lines/caps/arrow.ts index a91c0221d3..c9cb9f3e26 100644 --- a/packages/typegpu-geometry/src/lines/caps/arrow.ts +++ b/packages/typegpu-geometry/src/lines/caps/arrow.ts @@ -1,36 +1,25 @@ import { vec2f } from 'typegpu/data'; -import type { v2f } from 'typegpu/data'; -import { addMul, rot90ccw, rot90cw } from '../../utils.ts'; -import { capShell } from './common.ts'; +import { add, mul, neg, normalize, sign } from 'typegpu/std'; +import { addMul, cross2d, rot90ccw } from '../../utils.ts'; +import type { JoinInput } from '../types.ts'; -export const arrowCap = capShell( - ( - vertexIndex, - joinPath, - V, - vu, - vd, - _right, - dir, - _left, - ) => { - 'use gpu'; - const dirRight = rot90cw(dir); - const dirLeft = rot90ccw(dir); - - const v0 = addMul(vu, dir, -7.5 * V.radius); - const v1 = addMul(V.position, addMul(dirRight, dir, -3), 3 * V.radius); - const v2 = addMul(V.position, vec2f(0, 0), 2 * V.radius); - const v3 = addMul(V.position, addMul(dirLeft, dir, -3), 3 * V.radius); - const v4 = addMul(vd, dir, -7.5 * V.radius); - const points = [v0, v1, v2, v3, v4]; - - if (joinPath.depth >= 0) { - const remove = [v0, v4]; - const dm = remove[joinPath.joinIndex & 0x1] as v2f; - return dm; - } - - return points[vertexIndex % 5] as v2f; - }, -); +export function arrow( + join: JoinInput, + joinIndex: number, + _maxJoinCount: number, +) { + 'use gpu'; + const bw = neg(normalize(join.fw)); + const vert = rot90ccw(bw); + const sgn = sign(cross2d(bw, join.d)); + const svert = mul(vert, sgn); + const v0 = add(svert, mul(bw, 7.5)); + const v1 = addMul(v0, add(bw, svert), 1.5); + if (joinIndex === 0) { + return addMul(join.C.position, v0, join.C.radius); + } + if (joinIndex === 1) { + return addMul(join.C.position, v1, join.C.radius); + } + return vec2f(join.C.position); +} diff --git a/packages/typegpu-geometry/src/lines/caps/butt.ts b/packages/typegpu-geometry/src/lines/caps/butt.ts index 81c91b8a48..13ad690cfc 100644 --- a/packages/typegpu-geometry/src/lines/caps/butt.ts +++ b/packages/typegpu-geometry/src/lines/caps/butt.ts @@ -1,60 +1,16 @@ -import { vec2f } from 'typegpu/data'; -import type { v2f } from 'typegpu/data'; -import { dot, select } from 'typegpu/std'; -import { addMul, rot90ccw, rot90cw } from '../../utils.ts'; -import { intersectTangent, miterPointNoCheck } from '../utils.ts'; -import { capShell } from './common.ts'; +import { mul, normalize, sign } from 'typegpu/std'; +import { addMul, cross2d, rot90ccw } from '../../utils.ts'; +import type { JoinInput } from '../types.ts'; -export const buttCap = capShell( - ( - vertexIndex, - joinPath, - V, - vu, - vd, - right, - dir, - left, - ) => { - 'use gpu'; - const shouldJoin = dot(dir, right) < 0; - const dirRight = rot90cw(dir); - const dirLeft = rot90ccw(dir); - const u = select( - intersectTangent(right, dirRight), - dirRight, - shouldJoin, - ); - const c = vec2f(0, 0); - const d = select( - intersectTangent(left, dirLeft), - dirLeft, - shouldJoin, - ); - - const joinIndex = joinPath.joinIndex; - if (joinPath.depth >= 0) { - const miterR = select( - u, - miterPointNoCheck(right, dirRight), - shouldJoin, - ); - const miterL = select( - d, - miterPointNoCheck(dirLeft, left), - shouldJoin, - ); - const parents = [miterR, miterL]; - const dm = parents[joinIndex & 0b1] as v2f; - return addMul(V.position, dm, V.radius); - } - - const v1 = addMul(V.position, u, V.radius); - const v0 = select(v1, vu, shouldJoin); - const v2 = addMul(V.position, c, V.radius); - const v3 = addMul(V.position, d, V.radius); - const v4 = select(v3, vd, shouldJoin); - const points = [v0, v1, v2, v3, v4]; - return points[vertexIndex % 5] as v2f; - }, -); +export function butt( + join: JoinInput, + _joinIndex: number, + _maxJoinCount: number, +) { + 'use gpu'; + const fw = normalize(join.fw); + const vert = rot90ccw(fw); + const sgn = sign(cross2d(fw, join.d)); + const svert = mul(vert, sgn); + return addMul(join.C.position, svert, join.C.radius); +} diff --git a/packages/typegpu-geometry/src/lines/caps/common.ts b/packages/typegpu-geometry/src/lines/caps/common.ts deleted file mode 100644 index e6949f6b1b..0000000000 --- a/packages/typegpu-geometry/src/lines/caps/common.ts +++ /dev/null @@ -1,14 +0,0 @@ -import tgpu from 'typegpu'; -import { u32, vec2f } from 'typegpu/data'; -import { JoinPath, LineSegmentVertex } from '../types.ts'; - -export const capShell = tgpu.fn([ - u32, - JoinPath, - LineSegmentVertex, - vec2f, - vec2f, - vec2f, - vec2f, - vec2f, -], vec2f); diff --git a/packages/typegpu-geometry/src/lines/caps/index.ts b/packages/typegpu-geometry/src/lines/caps/index.ts index 2b969d8b26..673e5ac147 100644 --- a/packages/typegpu-geometry/src/lines/caps/index.ts +++ b/packages/typegpu-geometry/src/lines/caps/index.ts @@ -1,15 +1,6 @@ -import { buttCap } from './butt.ts'; -import { squareCap } from './square.ts'; -import { roundCap } from './round.ts'; -import { triangleCap } from './triangle.ts'; -import { arrowCap } from './arrow.ts'; -import { swallowtailCap } from './swallowtail.ts'; - -export const lineCaps = { - butt: buttCap, - square: squareCap, - round: roundCap, - triangle: triangleCap, - arrow: arrowCap, - swallowtail: swallowtailCap, -}; +export { round } from '../joins/round.ts'; +export { arrow } from './arrow.ts'; +export { butt } from './butt.ts'; +export { square } from './square.ts'; +export { triangle } from './triangle.ts'; +export { wedge } from './wedge.ts'; diff --git a/packages/typegpu-geometry/src/lines/caps/round.ts b/packages/typegpu-geometry/src/lines/caps/round.ts deleted file mode 100644 index b2975cb66e..0000000000 --- a/packages/typegpu-geometry/src/lines/caps/round.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { v2f } from 'typegpu/data'; -import { select } from 'typegpu/std'; -import { addMul, bisectCcw, bisectNoCheck } from '../../utils.ts'; -import { capShell } from './common.ts'; - -export const roundCap = capShell( - ( - vertexIndex, - joinPath, - V, - vu, - vd, - right, - dir, - left, - ) => { - 'use gpu'; - const uR = right; - const u = dir; - const c = dir; - const d = dir; - const dR = left; - - const joinIndex = joinPath.joinIndex; - if (joinPath.depth >= 0) { - const parents = [uR, u, d, dR]; - let d0 = parents[(joinIndex * 2) & 3] as v2f; - let d1 = parents[(joinIndex * 2 + 1) & 3] as v2f; - let dm = bisectCcw(d0, d1); - let path = joinPath.path; - for (let depth = joinPath.depth; depth > 0; depth -= 1) { - const isLeftChild = (path & 1) === 0; - d0 = select(dm, d0, isLeftChild); - d1 = select(d1, dm, isLeftChild); - dm = bisectNoCheck(d0, d1); - path >>= 1; - } - return addMul(V.position, dm, V.radius); - } - - const v1 = addMul(V.position, u, V.radius); - const v2 = addMul(V.position, c, V.radius); - const v3 = addMul(V.position, d, V.radius); - const points = [vu, v1, v2, v3, vd]; - return points[vertexIndex % 5] as v2f; - }, -); diff --git a/packages/typegpu-geometry/src/lines/caps/square.ts b/packages/typegpu-geometry/src/lines/caps/square.ts index 93accec54d..4fad63de8d 100644 --- a/packages/typegpu-geometry/src/lines/caps/square.ts +++ b/packages/typegpu-geometry/src/lines/caps/square.ts @@ -1,57 +1,23 @@ -import type { v2f } from 'typegpu/data'; -import { add, dot, select } from 'typegpu/std'; -import { addMul, rot90ccw, rot90cw } from '../../utils.ts'; -import { miterPointNoCheck } from '../utils.ts'; -import { capShell } from './common.ts'; +import { vec2f } from 'typegpu/data'; +import { normalize, select, sign } from 'typegpu/std'; +import { addMul, cross2d, rot90ccw } from '../../utils.ts'; +import type { JoinInput } from '../types.ts'; -export const squareCap = capShell( - ( - vertexIndex, - joinPath, - V, - vu, - vd, - right, - dir, - left, - ) => { - 'use gpu'; - const shouldJoin = dot(dir, right) < 0; - const dirRight = rot90cw(dir); - const dirLeft = rot90ccw(dir); - const u = select( - miterPointNoCheck(right, dir), - add(dir, dirRight), - shouldJoin, - ); - const c = dir; - const d = select( - miterPointNoCheck(dir, left), - add(dir, dirLeft), - shouldJoin, - ); - - const joinIndex = joinPath.joinIndex; - if (joinPath.depth >= 0) { - const miterR = select( - right, - miterPointNoCheck(right, dirRight), - shouldJoin, - ); - const miterL = select( - left, - miterPointNoCheck(dirLeft, left), - shouldJoin, - ); - const parents = [miterR, miterL]; - const dm = parents[joinIndex & 0b1] as v2f; - return addMul(V.position, dm, V.radius); - } - - const v1 = addMul(V.position, u, V.radius); - const v2 = addMul(V.position, c, V.radius); - const v3 = addMul(V.position, d, V.radius); - const points = [vu, v1, v2, v3, vd]; - return points[vertexIndex % 5] as v2f; - }, -); +export function square( + join: JoinInput, + joinIndex: number, + _maxJoinCount: number, +) { + 'use gpu'; + if (joinIndex === 0) { + return vec2f(join.v); + } + const fw = normalize(join.fw); + const vert = rot90ccw(fw); + const sgn = sign(cross2d(fw, join.d)); + return addMul( + join.C.position, + select(addMul(fw, vert, sgn), fw, joinIndex > 1), + join.C.radius, + ); +} diff --git a/packages/typegpu-geometry/src/lines/caps/swallowtail.ts b/packages/typegpu-geometry/src/lines/caps/swallowtail.ts deleted file mode 100644 index 04dd7b22c3..0000000000 --- a/packages/typegpu-geometry/src/lines/caps/swallowtail.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { v2f } from 'typegpu/data'; -import { addMul, midPoint } from '../../utils.ts'; -import { add } from 'typegpu/std'; -import { capShell } from './common.ts'; - -export const swallowtailCap = capShell( - ( - vertexIndex, - joinPath, - V, - vu, - vd, - right, - dir, - left, - ) => { - 'use gpu'; - if (joinPath.depth >= 0) { - const remove = [right, left]; - const dm = remove[joinPath.joinIndex & 0x1] as v2f; - return addMul(V.position, dm, V.radius); - } - - const v1 = addMul(V.position, add(right, dir), V.radius); - const v2 = addMul(V.position, midPoint(right, left), V.radius); - const v3 = addMul(V.position, add(left, dir), V.radius); - const points = [vu, v1, v2, v3, vd]; - return points[vertexIndex % 5] as v2f; - }, -); diff --git a/packages/typegpu-geometry/src/lines/caps/triangle.ts b/packages/typegpu-geometry/src/lines/caps/triangle.ts index 1e43701cad..c2aea659b5 100644 --- a/packages/typegpu-geometry/src/lines/caps/triangle.ts +++ b/packages/typegpu-geometry/src/lines/caps/triangle.ts @@ -1,29 +1,17 @@ -import type { v2f } from 'typegpu/data'; +import { vec2f } from 'typegpu/data'; +import { normalize } from 'typegpu/std'; import { addMul } from '../../utils.ts'; -import { capShell } from './common.ts'; +import type { JoinInput } from '../types.ts'; -export const triangleCap = capShell( - ( - vertexIndex, - joinPath, - V, - vu, - vd, - right, - dir, - left, - ) => { - 'use gpu'; - if (joinPath.depth >= 0) { - const remove = [right, left]; - const dm = remove[joinPath.joinIndex & 0x1] as v2f; - return addMul(V.position, dm, V.radius); - } - - const v1 = addMul(V.position, right, V.radius); - const v2 = addMul(V.position, dir, V.radius); - const v3 = addMul(V.position, left, V.radius); - const points = [vu, v1, v2, v3, vd]; - return points[vertexIndex % 5] as v2f; - }, -); +export function triangle( + join: JoinInput, + joinIndex: number, + _maxJoinCount: number, +) { + 'use gpu'; + if (joinIndex === 0) { + return vec2f(join.v); + } + const fw = normalize(join.fw); + return addMul(join.C.position, fw, join.C.radius); +} diff --git a/packages/typegpu-geometry/src/lines/caps/wedge.ts b/packages/typegpu-geometry/src/lines/caps/wedge.ts new file mode 100644 index 0000000000..22556cdcb2 --- /dev/null +++ b/packages/typegpu-geometry/src/lines/caps/wedge.ts @@ -0,0 +1,19 @@ +import { vec2f } from 'typegpu/data'; +import { normalize, sign } from 'typegpu/std'; +import { addMul, cross2d, rot90ccw } from '../../utils.ts'; +import type { JoinInput } from '../types.ts'; + +export function wedge( + join: JoinInput, + joinIndex: number, + _maxJoinCount: number, +) { + 'use gpu'; + if (joinIndex === 0) { + return vec2f(join.v); + } + const fw = normalize(join.fw); + const vert = rot90ccw(fw); + const sgn = sign(cross2d(fw, join.d)); + return addMul(join.C.position, addMul(fw, vert, sgn), join.C.radius); +} diff --git a/packages/typegpu-geometry/src/lines/constants.ts b/packages/typegpu-geometry/src/lines/constants.ts index e0233f7ed5..2f8707ee80 100644 --- a/packages/typegpu-geometry/src/lines/constants.ts +++ b/packages/typegpu-geometry/src/lines/constants.ts @@ -1,4 +1,6 @@ -import tgpu from 'typegpu'; -import { f32 } from 'typegpu/data'; - -export const JOIN_LIMIT = tgpu['~unstable'].const(f32, 0.999); +/** + * Limit between two neighboring tangent normals beyond which + * a miter is used for the join. This prevents tiny join triangles + * at almost-straight segment pairs. + */ +export const MITER_DOT_PRODUCT_LIMIT = 0.99; diff --git a/packages/typegpu-geometry/src/lines/externalNormals.ts b/packages/typegpu-geometry/src/lines/externalNormals.ts new file mode 100644 index 0000000000..98ebdfd50f --- /dev/null +++ b/packages/typegpu-geometry/src/lines/externalNormals.ts @@ -0,0 +1,39 @@ +import tgpu from 'typegpu'; +import type { Infer } from 'typegpu/data'; +import { f32, struct, vec2f } from 'typegpu/data'; +import { dot, max, sqrt } from 'typegpu/std'; + +export type ExternalNormals = Infer; +export const ExternalNormals = struct({ + /** Normal which is CW (left of) the distance vector */ + nL: vec2f, + /** Normal which is CCW (right of) the distance vector */ + nR: vec2f, +}); + +/** + * Given two circles at a `distance` of radii `r1` and `r2`, + * computes the two external tangent normals, which correspond to + * line segment edges. + * + * NOTE: for circles with the same radius, this corresponds to + * normalizing the distance vector and rotating CW (nL) and CCW (nR). + */ +export const externalNormals = tgpu.fn( + [vec2f, f32, f32], + ExternalNormals, +)((distance, r1, r2) => { + // Distance squared inverse is used to avoid taking square root more than necessary. + // This way we only need to take it once! + const dist2Inv = 1 / dot(distance, distance); + const cosMulLen = r1 - r2; + const cosDivLen = cosMulLen * dist2Inv; + const sinDivLen = sqrt(max(0, 1 - cosMulLen * cosDivLen) * dist2Inv); + const a = distance.x * cosDivLen; + const b = distance.y * sinDivLen; + const c = distance.x * sinDivLen; + const d = distance.y * cosDivLen; + const nL = vec2f(a - b, c + d); + const nR = vec2f(a + b, -c + d); + return ExternalNormals({ nL, nR }); +}); diff --git a/packages/typegpu-geometry/src/lines/index.ts b/packages/typegpu-geometry/src/lines/index.ts new file mode 100644 index 0000000000..779d96c99d --- /dev/null +++ b/packages/typegpu-geometry/src/lines/index.ts @@ -0,0 +1,5 @@ +export * as caps from './caps/index.ts'; +export * from './indices.ts'; +export * as joins from './joins/index.ts'; +export * from './lines.ts'; +export * from './types.ts'; diff --git a/packages/typegpu-geometry/src/lines/indices.ts b/packages/typegpu-geometry/src/lines/indices.ts index ddbcb87a16..b2d4f5e6ca 100644 --- a/packages/typegpu-geometry/src/lines/indices.ts +++ b/packages/typegpu-geometry/src/lines/indices.ts @@ -1,146 +1,88 @@ -// deno-fmt-ignore -export const lineSegmentIndicesCapLevel0 = [ - 0, 4, 5, - 1, 2, 0, - 2, 3, 4, - 4, 0, 2, - 5, 9, 0, - 6, 7, 5, - 7, 8, 9, - 9, 5, 7, -] - -// deno-fmt-ignore -export const lineSegmentIndicesCapLevel1 = [ - ...lineSegmentIndicesCapLevel0, - 10, 1, 0, - 11, 4, 3, - 12, 6, 5, - 13, 9, 8, -] - -// deno-fmt-ignore -export const lineSegmentIndicesCapLevel2 = [ - ...lineSegmentIndicesCapLevel1, - 14, 10, 0, - 15, 1, 10, - 16, 11, 3, - 17, 4, 11, - 18, 12, 5, - 19, 6, 12, - 20, 13, 8, - 21, 9, 13, -]; +/** + * Line segment triangulation: + * 0 + * 3/ | \2 + * |\ |\ | + * | \| \| + * 4\ | /5 + * 1 + * + * Joins are added like: (only top shown) + * 15---0---14 + * |/ /|\ \| + * 11 / | \ 10 + * |/ /|\ \| + * 7 / | \ 6 + * |/ | \| + * 3 | 2 + */ // deno-fmt-ignore -export const lineSegmentIndicesCapLevel3 = [ - ...lineSegmentIndicesCapLevel2, - 22, 14, 0, - 23, 10, 14, - 24, 15, 10, - 25, 1, 15, - 26, 16, 3, - 27, 11, 16, - 28, 17, 11, - 29, 4, 17, - 30, 18, 5, - 31, 12, 18, - 32, 19, 12, - 33, 6, 19, - 34, 20, 8, - 35, 13, 20, - 36, 21, 13, - 37, 9, 21, +const lineSegmentIndicesBase = [ + 0, 5, 2, + 0, 3, 1, + 1, 3, 4, + 1, 5, 0, ]; // deno-fmt-ignore -export const lineSegmentWireframeIndicesCapLevel0 = [ +const lineSegmentWireframeIndicesBase = [ 0, 1, 0, 2, - 0, 4, + 0, 3, 0, 5, - 0, 9, - 1, 2, - 2, 3, - 2, 4, + 1, 3, + 1, 4, + 1, 5, + 2, 5, 3, 4, - 4, 5, - 5, 6, - 5, 7, - 5, 9, - 6, 7, - 7, 8, - 7, 9, - 8, 9, ]; -// deno-fmt-ignore -export const lineSegmentWireframeIndicesCapLevel1 = [ - ...lineSegmentWireframeIndicesCapLevel0, - 0, 10, - 1, 10, - 3, 11, - 4, 11, - 5, 12, - 6, 12, - 8, 13, - 9, 13, -] +export function lineSegmentIndices(joinTriangleCount: number) { + const indices = [...lineSegmentIndicesBase]; + for (let i = 0; i < joinTriangleCount; ++i) { + for (let j = 0; j < 4; ++j) { + const vertexIndex = i * 4 + j + 6; + const end = j < 2 ? 0 : 1; + const a = j % 2 === 0 ? end : vertexIndex - 4; + const b = j % 2 === 0 ? vertexIndex - 4 : end; + indices.push(vertexIndex, a, b); + } + } + return indices; +} -// deno-fmt-ignore -export const lineSegmentWireframeIndicesCapLevel2 = [ - ...lineSegmentWireframeIndicesCapLevel1, - 0, 14, - 14, 10, - 10, 15, - 15, 1, - 3, 16, - 16, 11, - 11, 17, - 17, 4, - 5, 18, - 18, 12, - 12, 19, - 19, 6, - 8, 20, - 20, 13, - 13, 21, - 21, 9, -]; +export function lineSegmentWireframeIndices(joinTriangleCount: number) { + const wireframeIndices = [...lineSegmentWireframeIndicesBase]; + for (let i = 0; i < joinTriangleCount; ++i) { + for (let j = 0; j < 4; ++j) { + const vertexIndex = i * 4 + j + 6; + const end = j < 2 ? 0 : 1; + const a = j % 2 === 0 ? end : vertexIndex - 4; + const b = j % 2 === 0 ? vertexIndex - 4 : end; + wireframeIndices.push(vertexIndex, a); + wireframeIndices.push(vertexIndex, b); + } + } + return wireframeIndices; +} // deno-fmt-ignore -export const lineSegmentWireframeIndicesCapLevel3 = [ - ...lineSegmentWireframeIndicesCapLevel2, - 0, 22, - 22, 14, - 14, 23, - 23, 10, - 10, 24, - 24, 15, - 15, 25, - 25, 1, - 3, 26, - 26, 16, - 16, 27, - 27, 11, - 11, 28, - 28, 17, - 17, 29, - 29, 4, - 5, 30, - 30, 18, - 18, 31, - 31, 12, - 12, 32, - 32, 19, - 19, 33, - 33, 6, - 8, 34, - 34, 20, - 20, 35, - 35, 13, - 13, 36, - 36, 21, - 21, 37, - 37, 9, +const lineSegmentLeftIndicesBase = [ + 0, 3, 1, + 1, 3, 4, ]; + +export function lineSegmentLeftIndices(joinTriangleCount: number) { + const indices = [...lineSegmentLeftIndicesBase]; + for (let i = 0; i < joinTriangleCount; ++i) { + for (let j = 1; j < 3; ++j) { + const vertexIndex = i * 4 + j + 6; + const end = j < 2 ? 0 : 1; + const a = j % 2 === 0 ? end : vertexIndex - 4; + const b = j % 2 === 0 ? vertexIndex - 4 : end; + indices.push(vertexIndex, a, b); + } + } + return indices; +} diff --git a/packages/typegpu-geometry/src/lines/joins/common.ts b/packages/typegpu-geometry/src/lines/joins/common.ts deleted file mode 100644 index 9bc04dc9ca..0000000000 --- a/packages/typegpu-geometry/src/lines/joins/common.ts +++ /dev/null @@ -1,46 +0,0 @@ -import tgpu from 'typegpu'; -import { bool, u32, vec2f } from 'typegpu/data'; -import { dot } from 'typegpu/std'; -import { cross2d } from '../../utils.ts'; -import { isCCW, rank3 } from '../utils.ts'; -import { JoinPath, LineSegmentVertex } from '../types.ts'; - -export const joinShell = tgpu.fn([ - u32, - u32, - JoinPath, - LineSegmentVertex, - vec2f, - vec2f, - vec2f, - vec2f, - vec2f, - vec2f, - bool, - bool, -], vec2f); - -export const joinSituationIndex = tgpu.fn( - [vec2f, vec2f, vec2f, vec2f], - u32, -)( - (ul, ur, dl, dr) => { - // ur is the reference vector - // we find all 6 orderings of the remaining ul, dl, dr - const crossUL = cross2d(ur, ul); - const crossDL = cross2d(ur, dl); - const crossDR = cross2d(ur, dr); - const signUL = crossUL >= 0; - const signDL = crossDL >= 0; - const signDR = crossDR >= 0; - const dotUL = dot(ur, ul); - const dotDL = dot(ur, dl); - const dotDR = dot(ur, dr); - - return rank3( - isCCW(dotUL, signUL, dotDL, signDL), - isCCW(dotDL, signDL, dotDR, signDR), - isCCW(dotUL, signUL, dotDR, signDR), - ); - }, -); diff --git a/packages/typegpu-geometry/src/lines/joins/index.ts b/packages/typegpu-geometry/src/lines/joins/index.ts index 0c419d1691..d9e1565a4d 100644 --- a/packages/typegpu-geometry/src/lines/joins/index.ts +++ b/packages/typegpu-geometry/src/lines/joins/index.ts @@ -1,7 +1,2 @@ -import { miterJoin } from './miter.ts'; -import { roundJoin } from './round.ts'; - -export const lineJoins = { - miter: miterJoin, - round: roundJoin, -}; +export { miter } from './miter.ts'; +export { round } from './round.ts'; diff --git a/packages/typegpu-geometry/src/lines/joins/miter.ts b/packages/typegpu-geometry/src/lines/joins/miter.ts index b2ed59962e..0347ad8a63 100644 --- a/packages/typegpu-geometry/src/lines/joins/miter.ts +++ b/packages/typegpu-geometry/src/lines/joins/miter.ts @@ -1,95 +1,70 @@ import tgpu from 'typegpu'; -import { add, dot, mul, normalize, select } from 'typegpu/std'; -import { addMul, bisectCcw } from '../../utils.ts'; -import { intersectLines, miterPoint } from '../utils.ts'; -import { joinShell } from './common.ts'; -import { f32, type v2f, vec2f } from 'typegpu/data'; +import type { v2f } from 'typegpu/data'; +import { vec2f } from 'typegpu/data'; +import { dot, mul, normalize, select } from 'typegpu/std'; +import { + addMul, + bisectCcw, + cross2d, + miterPointNoCheck, + rot90ccw, +} from '../../utils.ts'; +import type { JoinInput } from '../types.ts'; export const miterJoinLimitSlot = tgpu.slot(2); /** - * Limits the miter point to the given limit ratio, which is - * a length relative to the line vertex radius. + * Finds the miter point of tangents to two points on a circle. */ -export const miterLimit = tgpu.fn([vec2f, f32], vec2f)((miter, limitRatio) => { - const m2 = dot(miter, miter); - if (m2 > limitRatio * limitRatio) { - return mul( - normalize(miter), - (limitRatio - 1) * (limitRatio * limitRatio - 1) / (m2 - 1) + 1, - ); +function miterPointBisectorWhenClockwise(a: v2f, b: v2f) { + 'use gpu'; + const sin_ = cross2d(a, b); + if (sin_ <= 0) { + return bisectCcw(a, b); } - return miter; -}); - -export const miterJoin = joinShell( - ( - situationIndex, - vertexIndex, - joinPath, - V, - vu, - vd, - ul, - ur, - dl, - dr, - joinU, - joinD, - ) => { - 'use gpu'; - let miterU = miterPoint(ur, ul); - let miterD = miterPoint(dl, dr); - miterU = miterLimit(miterU, miterJoinLimitSlot.$); - miterD = miterLimit(miterD, miterJoinLimitSlot.$); - - const shouldCross = situationIndex === 1 || situationIndex === 4; - const crossCenter = intersectLines(ul, dl, ur, dr).point; - const averageCenter = mul( - add( - normalize(miterU), - normalize(miterD), - ), - 0.5, - ); + return miterPointNoCheck(a, b); +} - let uR = ur; - let u = miterU; - let c = select(averageCenter, crossCenter, shouldCross); - let d = miterD; - let dR = dr; - - if (situationIndex === 2) { - const mid = bisectCcw(ur, dr); - uR = ur; - u = mid; - c = mid; - d = mid; - dR = dr; - } - - if (situationIndex === 3) { - const mid = bisectCcw(dl, ul); - uR = ur; - u = mid; - c = mid; - d = mid; - dR = dr; - } +/** + * Finds the miter point of tangents to two points on respective circles. + */ +function miterPoint(a: v2f, b: v2f) { + 'use gpu'; + const sin_ = cross2d(a, b); + const b2 = dot(b, b); + const cos_ = dot(a, b); + const diff = b2 - cos_; + const t = diff / sin_; + return addMul(a, rot90ccw(a), t); +} - const joinIndex = joinPath.joinIndex; - if (joinPath.depth >= 0) { - const parents = [uR, u, d, dR]; - const d0 = parents[(joinIndex * 2) & 3] as v2f; - const d1 = parents[(joinIndex * 2 + 1) & 3] as v2f; - const dm = miterPoint(d0, d1); - return addMul(V.position, dm, V.radius); - } +/** + * Limits the miter point to the given limit ratio, which is + * a length relative to the control point radius. + */ +function miterLimit(miter: v2f, limitRatio: number) { + 'use gpu'; + const m2 = dot(miter, miter); + const l2 = limitRatio * limitRatio; + if (m2 > l2) { + return mul(normalize(miter), (limitRatio - 1) * (l2 - 1) / (m2 - 1) + 1); + } + return vec2f(miter); +} - const v1 = select(vu, addMul(V.position, u, V.radius), joinU); - const v2 = select(vu, addMul(V.position, c, V.radius), joinU || joinD); - const v3 = select(vd, addMul(V.position, d, V.radius), joinD); - const points = [vu, v1, v2, v3, vd]; - return points[vertexIndex % 5] as v2f; - }, -); +export function miter( + join: JoinInput, + joinIndex: number, + _maxJoinCount: number, +) { + 'use gpu'; + if (joinIndex === 0) { + return vec2f(join.v); + } + const miter = miterLimit( + miterPointBisectorWhenClockwise(join.start, join.end), + miterJoinLimitSlot.$, + ); + const dir = select(miterPoint(join.d, miter), miter, joinIndex > 1); + return addMul(join.C.position, dir, join.C.radius); +} diff --git a/packages/typegpu-geometry/src/lines/joins/round.ts b/packages/typegpu-geometry/src/lines/joins/round.ts index b77cc0bace..bbd7704af4 100644 --- a/packages/typegpu-geometry/src/lines/joins/round.ts +++ b/packages/typegpu-geometry/src/lines/joins/round.ts @@ -1,83 +1,20 @@ -import type { v2f } from 'typegpu/data'; -import { add, mul, select } from 'typegpu/std'; -import { addMul, bisectCcw, bisectNoCheck } from '../../utils.ts'; -import { intersectLines } from '../utils.ts'; -import { joinShell } from './common.ts'; +import { vec2f } from 'typegpu/data'; +import { addMul, bisectCcw, slerpApprox } from '../../utils.ts'; +import type { JoinInput } from '../types.ts'; -export const roundJoin = joinShell( - ( - situationIndex, - vertexIndex, - joinPath, - V, - vu, - vd, - ul, - ur, - dl, - dr, - joinU, - joinD, - ) => { - 'use gpu'; - const midU = bisectCcw(ur, ul); - const midD = bisectCcw(dl, dr); - const midR = bisectCcw(ur, dr); - const midL = bisectCcw(dl, ul); - - const shouldCross = situationIndex === 1 || situationIndex === 4; - const crossCenter = intersectLines(ul, dl, ur, dr).point; - const averageCenter = mul( - add( - add(ur, ul), - add(dl, dr), - ), - 0.25, - ); - - let uR = ur; - let u = midU; - let c = select(averageCenter, crossCenter, shouldCross); - let d = midD; - let dR = dr; - - if (situationIndex === 2) { - uR = ur; - u = midR; - c = midR; - d = midR; - dR = dr; - } - - if (situationIndex === 3) { - uR = ur; - u = midL; - c = midL; - d = midL; - dR = dr; - } - - const joinIndex = joinPath.joinIndex; - if (joinPath.depth >= 0) { - const parents = [uR, u, d, dR]; - let d0 = parents[(joinIndex * 2) & 3] as v2f; - let d1 = parents[(joinIndex * 2 + 1) & 3] as v2f; - let dm = bisectCcw(d0, d1); - let path = joinPath.path; - for (let depth = joinPath.depth; depth > 0; depth -= 1) { - const isLeftChild = (path & 1) === 0; - d0 = select(dm, d0, isLeftChild); - d1 = select(d1, dm, isLeftChild); - dm = bisectNoCheck(d0, d1); - path >>= 1; - } - return addMul(V.position, dm, V.radius); - } - - const v1 = select(vu, addMul(V.position, u, V.radius), joinU); - const v2 = select(vu, addMul(V.position, c, V.radius), joinU || joinD); - const v3 = select(vd, addMul(V.position, d, V.radius), joinD); - const points = [vu, v1, v2, v3, vd]; - return points[vertexIndex % 5] as v2f; - }, -); +export function round( + join: JoinInput, + joinIndex: number, + maxJoinCount: number, +) { + 'use gpu'; + if (joinIndex === 0) { + return vec2f(join.v); + } + const dir = slerpApprox( + join.d, + bisectCcw(join.start, join.end), + joinIndex / maxJoinCount, + ); + return addMul(join.C.position, dir, join.C.radius); +} diff --git a/packages/typegpu-geometry/src/lines/lines.ts b/packages/typegpu-geometry/src/lines/lines.ts index 64de96c08d..772170668a 100644 --- a/packages/typegpu-geometry/src/lines/lines.ts +++ b/packages/typegpu-geometry/src/lines/lines.ts @@ -1,59 +1,31 @@ import tgpu from 'typegpu'; -import { struct, u32, vec2f } from 'typegpu/data'; -import type { v2f } from 'typegpu/data'; -import { dot, mul, normalize, sub } from 'typegpu/std'; -import { addMul, midPoint, rot90ccw, rot90cw } from '../utils.ts'; -import { externalNormals, limitTowardsMiddle, miterPoint } from './utils.ts'; -import { JoinPath, LineSegmentVertex } from './types.ts'; -import { joinSituationIndex } from './joins/common.ts'; -import { roundJoin } from './joins/round.ts'; -import { roundCap } from './caps/round.ts'; -import { JOIN_LIMIT } from './constants.ts'; - -export const joinSlot = tgpu.slot(roundJoin); -export const startCapSlot = tgpu.slot(roundCap); -export const endCapSlot = tgpu.slot(roundCap); - -const getJoinParent = tgpu.fn([u32], u32)((i) => (i - 4) >> 1); - -const getJoinVertexPath = tgpu.fn([u32], JoinPath)((vertexIndex) => { - // deno-fmt-ignore - const lookup = [u32(0), u32(0), /* dont care */u32(0), u32(1), u32(1), u32(2), u32(2), /* dont care */u32(2), u32(3), u32(3)]; - if (vertexIndex < 10) { - return JoinPath({ - joinIndex: lookup[vertexIndex] as number, - path: 0, - depth: -1, - }); - } - let joinIndex = vertexIndex - 10; - let depth = 0; - let path = u32(0); - while (joinIndex >= 4) { - path = (path << 1) | (joinIndex & 1); - joinIndex = getJoinParent(joinIndex); - depth += 1; - } - return JoinPath({ joinIndex, path, depth }); -}); - -const LineSegmentOutput = struct({ - vertexPosition: vec2f, - situationIndex: u32, -}); - -export const lineSegmentVariableWidth = tgpu.fn([ - u32, - LineSegmentVertex, - LineSegmentVertex, - LineSegmentVertex, - LineSegmentVertex, -], LineSegmentOutput)((vertexIndex, A, B, C, D) => { - const joinPath = getJoinVertexPath(vertexIndex); - +import { u32, vec2f } from 'typegpu/data'; +import { dot, neg, select, sub } from 'typegpu/std'; +import { addMul, intersectLines } from '../utils.ts'; +import { ExternalNormals, externalNormals } from './externalNormals.ts'; +import { round } from './joins/round.ts'; +import { solveJoin } from './solveJoin.ts'; +import { JoinInput, LineControlPoint, LineSegmentOutput } from './types.ts'; + +export const joinSlot = tgpu.slot(round); +export const startCapSlot = tgpu.slot(round); +export const endCapSlot = tgpu.slot(round); + +export const lineSegmentVariableWidth = tgpu.fn( + [ + u32, + LineControlPoint, + LineControlPoint, + LineControlPoint, + LineControlPoint, + u32, + ], + LineSegmentOutput, +)((vertexIndex, A, B, C, D, maxJoinCount) => { const AB = sub(B.position, A.position); const BC = sub(C.position, B.position); - const CD = sub(D.position, C.position); + const DC = sub(C.position, D.position); + const CB = neg(BC); const radiusABDelta = A.radius - B.radius; const radiusBCDelta = B.radius - C.radius; @@ -62,145 +34,89 @@ export const lineSegmentVariableWidth = tgpu.fn([ // segments where one end completely contains the other are skipped // TODO: we should probably render a circle in some cases if (dot(BC, BC) <= radiusBCDelta * radiusBCDelta) { - return { - vertexPosition: vec2f(0, 0), - uv: vec2f(0, 0), - situationIndex: 0, - }; + return { vertexPosition: vec2f(0, 0), w: 1 }; } - const isCapB = dot(AB, AB) <= radiusABDelta * radiusABDelta + 1e-12; - const isCapC = dot(CD, CD) <= radiusCDDelta * radiusCDDelta + 1e-12; + const isCapB = dot(AB, AB) <= radiusABDelta * radiusABDelta; + const isCapC = dot(DC, DC) <= radiusCDDelta * radiusCDDelta; const eAB = externalNormals(AB, A.radius, B.radius); const eBC = externalNormals(BC, B.radius, C.radius); - const eCD = externalNormals(CD, C.radius, D.radius); - - const nBC = normalize(BC); - const nCB = mul(nBC, -1); - - let d0 = eBC.n1; - let d4 = eBC.n2; - let d5 = eBC.n2; - let d9 = eBC.n1; - - const situationIndexB = joinSituationIndex(eAB.n1, eBC.n1, eAB.n2, eBC.n2); - const situationIndexC = joinSituationIndex(eCD.n2, eBC.n2, eCD.n1, eBC.n1); - let joinBu = true; - let joinBd = true; - let joinCu = true; - let joinCd = true; - if (!isCapB) { - if ( - situationIndexB === 1 || situationIndexB === 5 || - dot(eBC.n2, eAB.n2) > JOIN_LIMIT.$ - ) { - d4 = miterPoint(eBC.n2, eAB.n2); - joinBd = false; - } - if ( - situationIndexB === 4 || situationIndexB === 5 || - dot(eAB.n1, eBC.n1) > JOIN_LIMIT.$ - ) { - d0 = miterPoint(eAB.n1, eBC.n1); - joinBu = false; - } + const eCB = ExternalNormals({ nL: eBC.nR, nR: eBC.nL }); + const eDC = externalNormals(DC, D.radius, C.radius); + + const joinLimit = dot(eBC.nL, BC); + const joinB = solveJoin(AB, BC, eAB, eBC, joinLimit, isCapB); + const joinC = solveJoin(DC, CB, eDC, eCB, -joinLimit, isCapC); + const d2 = joinB.dL; + const d3 = joinB.dR; + const d4 = joinC.dL; + const d5 = joinC.dR; + + const v2orig = addMul(B.position, d2, B.radius); + const v3orig = addMul(B.position, d3, B.radius); + const v4orig = addMul(C.position, d4, C.radius); + const v5orig = addMul(C.position, d5, C.radius); + + const limL = intersectLines(B.position, v2orig, C.position, v5orig); + const limR = intersectLines(B.position, v3orig, C.position, v4orig); + + const v2 = select(v2orig, limL.point, limL.valid); + const v5 = select(v5orig, limL.point, limL.valid); + const v3 = select(v3orig, limR.point, limR.valid); + const v4 = select(v4orig, limR.point, limR.valid); + + if (vertexIndex === 0) { + return { vertexPosition: B.position, w: 1 / B.radius }; } - if (!isCapC) { - if ( - situationIndexC === 4 || situationIndexC === 5 || - dot(eCD.n2, eBC.n2) > JOIN_LIMIT.$ - ) { - d5 = miterPoint(eCD.n2, eBC.n2); - joinCd = false; - } - if ( - situationIndexC === 1 || situationIndexC === 5 || - dot(eBC.n1, eCD.n1) > JOIN_LIMIT.$ - ) { - d9 = miterPoint(eBC.n1, eCD.n1); - joinCu = false; - } + if (vertexIndex === 1) { + return { vertexPosition: C.position, w: 1 / C.radius }; } - let v0 = addMul(B.position, d0, B.radius); - let v4 = addMul(B.position, d4, B.radius); - let v5 = addMul(C.position, d5, C.radius); - let v9 = addMul(C.position, d9, C.radius); - - const midBC = midPoint(B.position, C.position); - const tBC1 = rot90cw(eBC.n1); - const tBC2 = rot90ccw(eBC.n2); - - const limU = limitTowardsMiddle(midBC, tBC1, v0, v9); - const limD = limitTowardsMiddle(midBC, tBC2, v4, v5); - v0 = limU.a; - v9 = limU.b; - v4 = limD.a; - v5 = limD.b; - - // after this point we need to process only one of the joins! - const isCSide = joinPath.joinIndex >= 2; - - let situationIndex = situationIndexB; - let V = B; - let isCap = isCapB; - let j1 = eAB.n1; - let j2 = eBC.n1; - let j3 = eAB.n2; - let j4 = eBC.n2; - let vu = v0; - let vd = v4; - let joinU = joinBu; - let joinD = joinBd; - if (isCSide) { - situationIndex = situationIndexC; - V = C; - isCap = isCapC; - j4 = eBC.n1; - j3 = eCD.n1; - j2 = eBC.n2; - j1 = eCD.n2; - vu = v5; - vd = v9; - joinU = joinCd; - joinD = joinCu; - } + const joinIndex = (vertexIndex - 2) & 0b11; + const joinCount = (vertexIndex - 2) >> 2; + let join = JoinInput(); - const joinIndex = joinPath.joinIndex; - if (vertexIndex >= 10) { - const shouldJoin = [u32(joinBu), u32(joinBd), u32(joinCd), u32(joinCu)]; - if (shouldJoin[joinIndex] === 0) { - const noJoinPoints = [v0, v4, v5, v9]; - const vertexPosition = noJoinPoints[joinIndex] as v2f; - return { - situationIndex, - vertexPosition, - }; - } + // deno-fmt-ignore + if (joinIndex === 0) { + join = JoinInput({ + C: B, v: v2, d: d2, shouldJoin: joinB.shouldJoinL, isCap: isCapB, fw: CB, + start: d2, + end: select(eAB.nL, d3, joinB.isHairpin || isCapB), + }); + } else if (joinIndex === 1) { + join = JoinInput({ + C: B, v: v3, d: d3, shouldJoin: joinB.shouldJoinR, isCap: isCapB, fw: CB, + start: select(eAB.nR, d2, joinB.isHairpin || isCapB), + end: d3, + }); + } else if (joinIndex === 2) { + join = JoinInput({ + C: C, v: v4, d: d4, shouldJoin: joinC.shouldJoinL, isCap: isCapC, fw: BC, + start: d4, + end: select(eDC.nL, d5, joinC.isHairpin || isCapC), + }); + } else { + join = JoinInput({ + C: C, v: v5, d: d5, shouldJoin: joinC.shouldJoinR, isCap: isCapC, fw: BC, + start: select(eDC.nR, d4, joinC.isHairpin || isCapC), + end: d5, + }); } - let vertexPosition = vec2f(); - - // deno-fmt-ignore - if (isCap) { - if (isCSide) { - vertexPosition = endCapSlot.$(vertexIndex, joinPath, V, vu, vd, j2, nBC, j4); + let vertexPosition = vec2f(join.v); + if (join.isCap) { + if (joinIndex < 2) { + vertexPosition = startCapSlot.$(join, joinCount, maxJoinCount); } else { - vertexPosition = startCapSlot.$(vertexIndex, joinPath, V, vu, vd, j2, nCB, j4); + vertexPosition = endCapSlot.$(join, joinCount, maxJoinCount); } - } else { - vertexPosition = joinSlot.$( - situationIndex, vertexIndex, - joinPath, - V, vu, vd, - j1, j2, j3, j4, - joinU, joinD - ); + } else if (join.shouldJoin) { + vertexPosition = joinSlot.$(join, joinCount, maxJoinCount); } - return { - situationIndex, - vertexPosition, - }; + // TODO: adjust for reverse miter + const w = select(1 / B.radius, 1 / C.radius, joinIndex >= 2); + + return { vertexPosition, w }; }); diff --git a/packages/typegpu-geometry/src/lines/solveJoin.ts b/packages/typegpu-geometry/src/lines/solveJoin.ts new file mode 100644 index 0000000000..c28f3b9f38 --- /dev/null +++ b/packages/typegpu-geometry/src/lines/solveJoin.ts @@ -0,0 +1,41 @@ +import type { Infer, v2f } from 'typegpu/data'; +import { bool, struct, vec2f } from 'typegpu/data'; +import { dot, normalize, select } from 'typegpu/std'; +import { miterPointNoCheck } from '../utils.ts'; +import { MITER_DOT_PRODUCT_LIMIT } from './constants.ts'; +import type { ExternalNormals } from './externalNormals.ts'; + +type JoinResult = Infer; +const JoinResult = struct({ + dL: vec2f, + dR: vec2f, + shouldJoinL: bool, + shouldJoinR: bool, + isHairpin: bool, +}); + +export function solveJoin( + AB: v2f, + BC: v2f, + eAB: ExternalNormals, + eBC: ExternalNormals, + joinLimit: number, + isCap: boolean, +) { + 'use gpu'; + const underLimitL = dot(eAB.nL, BC) < joinLimit; + const underLimitR = dot(eAB.nR, BC) < joinLimit; + const isHairpin = (dot(AB, BC) < 0 && underLimitL === underLimitR) || + dot(normalize(AB), normalize(BC)) < -MITER_DOT_PRODUCT_LIMIT; + const tooCloseToJoinL = dot(eAB.nL, eBC.nL) > MITER_DOT_PRODUCT_LIMIT; + const tooCloseToJoinR = dot(eAB.nR, eBC.nR) > MITER_DOT_PRODUCT_LIMIT; + const shouldJoinL = isHairpin || underLimitL && !tooCloseToJoinL; + const shouldJoinR = isHairpin || underLimitR && !tooCloseToJoinR; + + const dLMiter = miterPointNoCheck(eAB.nL, eBC.nL); + const dRMiter = miterPointNoCheck(eBC.nR, eAB.nR); + const dL = select(eBC.nL, dLMiter, !isCap && !shouldJoinL); + const dR = select(eBC.nR, dRMiter, !isCap && !shouldJoinR); + + return JoinResult({ dL, dR, shouldJoinL, shouldJoinR, isHairpin }); +} diff --git a/packages/typegpu-geometry/src/lines/types.ts b/packages/typegpu-geometry/src/lines/types.ts index 0be8e18467..feafae1d86 100644 --- a/packages/typegpu-geometry/src/lines/types.ts +++ b/packages/typegpu-geometry/src/lines/types.ts @@ -1,16 +1,34 @@ -import { f32, i32, struct, u32, vec2f } from 'typegpu/data'; import type { Infer } from 'typegpu/data'; +import { bool, f32, struct, vec2f } from 'typegpu/data'; -export type JoinPath = Infer; -export const JoinPath = struct({ - joinIndex: u32, - path: u32, - /** -1 for vertices on the original segment, >=0 for vertices inside the join */ - depth: i32, -}); - -export type LineSegmentVertex = Infer; -export const LineSegmentVertex = struct({ +export type LineControlPoint = Infer; +export const LineControlPoint = struct({ position: vec2f, radius: f32, }); + +export type LineSegmentOutput = Infer; +export const LineSegmentOutput = struct({ + vertexPosition: vec2f, + /** Homogeneous coordinate for scaling surface values */ + w: f32, +}); + +export type LineSegmentVertexData = Infer; +export const LineSegmentVertexData = struct({ + along: f32, + cross: f32, + join: f32, +}); + +export type JoinInput = Infer; +export const JoinInput = struct({ + C: LineControlPoint, + v: vec2f, + d: vec2f, + fw: vec2f, + start: vec2f, + end: vec2f, + shouldJoin: bool, + isCap: bool, +}); diff --git a/packages/typegpu-geometry/src/lines/utils.ts b/packages/typegpu-geometry/src/lines/utils.ts deleted file mode 100644 index d3e6496343..0000000000 --- a/packages/typegpu-geometry/src/lines/utils.ts +++ /dev/null @@ -1,175 +0,0 @@ -import tgpu from 'typegpu'; -import { arrayOf, bool, f32, struct, u32, vec2f } from 'typegpu/data'; -import { - add, - clamp, - dot, - length, - max, - mix, - mul, - normalize, - select, - sqrt, - sub, -} from 'typegpu/std'; -import { addMul, bisectCcw, cross2d, midPoint, rot90ccw } from '../utils.ts'; - -/** Intersects tangent to point on a circle `a` with line from center in direction `n`. */ -export const intersectTangent = tgpu.fn([vec2f, vec2f], vec2f)((a, n) => { - const cos_ = dot(a, n); - return mul(n, 1 / cos_); -}); - -/** - * Finds the miter point of tangents to two points on a circle. - * The miter point is on the smaller arc. - */ -export const miterPointNoCheck = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { - const ab = add(a, b); - return mul(ab, 2 / dot(ab, ab)); -}); - -/** - * Finds the miter point of tangents to two points on respective circles. - * The miter point is on the counter-clockwise arc between the circles if possible, - * otherwise at "infinity". - */ -export const miterPoint = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { - const sin_ = cross2d(a, b); - const bisection = bisectCcw(a, b); - const b2 = dot(b, b); - const cos_ = dot(a, b); - const diff = b2 - cos_; - // TODO: make this check relative - if (diff * diff < 1e-4) { - // the vectors are almost colinear - return midPoint(a, b); - } - if (sin_ < 0) { - // if the miter is at infinity, just make it super far - return mul(bisection, -1e6); - } - const t = diff / sin_; - return addMul(a, rot90ccw(a), t); -}); - -const ExternalNormals = struct({ - n1: vec2f, - n2: vec2f, -}); - -/** - * Computes external tangent directions (normals to tangent) - * for two circles at a `distance` with radii `r1` and `r2`. - */ -export const externalNormals = tgpu.fn( - [vec2f, f32, f32], - ExternalNormals, -)((distance, r1, r2) => { - const dNorm = normalize(distance); - const expCos = (r1 - r2) / length(distance); - const expSin = sqrt(max(0, 1 - expCos * expCos)); - const a = dNorm.x * expCos; - const b = dNorm.y * expSin; - const c = dNorm.x * expSin; - const d = dNorm.y * expCos; - const n1 = vec2f(a - b, c + d); - const n2 = vec2f(a + b, -c + d); - return ExternalNormals({ n1, n2 }); -}); - -const Intersection = struct({ - valid: bool, - t: f32, - point: vec2f, -}); - -export const intersectLines = tgpu.fn( - [vec2f, vec2f, vec2f, vec2f], - Intersection, -)( - (A1, A2, B1, B2) => { - const a = sub(A2, A1); - const b = sub(B2, B1); - const axb = cross2d(a, b); - const AB = sub(B1, A1); - const t = cross2d(AB, b) / axb; - return { - valid: axb !== 0, - t, - point: addMul(A1, a, t), - }; - }, -); - -const LimitAlongResult = struct({ - a: vec2f, - b: vec2f, - limitWasHit: bool, -}); - -/** - * Leaves a and b separate if no collision, otherwise merges them towards "middle". - */ -export const limitTowardsMiddle = tgpu.fn( - [vec2f, vec2f, vec2f, vec2f], - LimitAlongResult, -)( - (middle, dir, p1, p2) => { - const t1 = dot(sub(p1, middle), dir); - const t2 = dot(sub(p2, middle), dir); - if (t1 <= t2) { - return LimitAlongResult({ a: p1, b: p2, limitWasHit: false }); - } - const t = clamp(t1 / (t1 - t2), 0, 1); - const p = mix(p1, p2, t); - return LimitAlongResult({ a: p, b: p, limitWasHit: true }); - }, -); - -export const projectToLineSegment = tgpu.fn([vec2f, vec2f, vec2f], vec2f)( - (A, B, point) => { - const p = sub(point, A); - const AB = sub(B, A); - const t = clamp(dot(p, AB) / dot(AB, AB), 0, 1); - const projP = addMul(A, AB, t); - return projP; - }, -); - -export const uvToLineSegment = tgpu.fn( - [vec2f, vec2f, vec2f], - vec2f, -)( - (A, B, point) => { - const p = sub(point, A); - const AB = sub(B, A); - const x = dot(p, AB) / dot(AB, AB); - const y = cross2d(normalize(AB), p); - return vec2f(x, y); - }, -); - -const lookup = tgpu.const(arrayOf(u32, 8), [ - 5, // 000 c >= b >= a - 3, // 001 INVALID - 4, // 010 b > c >= a - 3, // 011 b >= a > c - 2, // 100 c >= a > b - 1, // 101 a > c >= b - 0, // 110 INVALID - 0, // 111 a > b > c -]); - -export const rank3 = tgpu.fn([bool, bool, bool], u32)((aGb, bGc, aGc) => { - const code = (u32(aGb) << 2) | (u32(bGc) << 1) | u32(aGc); - return lookup.$[code] as number; -}); - -export const isCCW = tgpu.fn([f32, bool, f32, bool], bool)( - (aX, aYSign, bX, bYSign) => { - const sameSide = aYSign === bYSign; - return select(aYSign, aYSign === (aX >= bX), sameSide); - }, -); diff --git a/packages/typegpu-geometry/src/utils.ts b/packages/typegpu-geometry/src/utils.ts index bb85865c85..c4311f63cd 100644 --- a/packages/typegpu-geometry/src/utils.ts +++ b/packages/typegpu-geometry/src/utils.ts @@ -1,6 +1,6 @@ import tgpu from 'typegpu'; -import { f32, vec2f } from 'typegpu/data'; -import { add, dot, mul, normalize, select } from 'typegpu/std'; +import { bool, f32, struct, vec2f } from 'typegpu/data'; +import { add, dot, mix, mul, normalize, select, sub } from 'typegpu/std'; /** Shorthand for `add(a, mul(b, f))` due to lack of operators */ export const addMul = tgpu.fn([vec2f, vec2f, f32], vec2f)((a, b, f) => { @@ -43,10 +43,19 @@ export const bisectCcw = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { return normalize(dir); }); +/** + * Finds the miter point of tangents to two points on a circle. + * The miter point is on the smaller arc. + */ +export const miterPointNoCheck = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { + const ab = add(a, b); + return mul(ab, 2 / dot(ab, ab)); +}); + /** * Finds bisector direction between two vectors. * There is no check done to be on the CW part, instead - * it is assumed that a and b are well less than 180 degrees apart. + * it is assumed that a and b are significantly less than 180 degrees apart. */ export const bisectNoCheck = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { return normalize(add(a, b)); @@ -55,3 +64,40 @@ export const bisectNoCheck = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { export const midPoint = tgpu.fn([vec2f, vec2f], vec2f)((a, b) => { return mul(0.5, add(a, b)); }); + +export const slerpApprox = tgpu.fn([vec2f, vec2f, f32], vec2f)((a, b, t) => { + const mid = bisectNoCheck(a, b); + let a_ = vec2f(a); + let b_ = vec2f(mid); + let t_ = 2 * t; + if (t > 0.5) { + a_ = vec2f(mid); + b_ = vec2f(b); + t_ -= 1; + } + return normalize(mix(a_, b_, t_)); +}); + +const Intersection = struct({ + valid: bool, + t: f32, + point: vec2f, +}); + +export const intersectLines = tgpu.fn( + [vec2f, vec2f, vec2f, vec2f], + Intersection, +)( + (A1, A2, B1, B2) => { + const a = sub(A2, A1); + const b = sub(B2, B1); + const axb = cross2d(a, b); + const AB = sub(B1, A1); + const t = cross2d(AB, b) / axb; + return { + valid: axb !== 0 && t >= 0 && t <= 1, + t, + point: addMul(A1, a, t), + }; + }, +);