-
-
Notifications
You must be signed in to change notification settings - Fork 62
docs: Rotating Triangle Tiles example #2048
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release
Are you sure you want to change the base?
Changes from all commits
ac288d7
74e8bea
02a5a8a
b42117e
d4feb08
5b21236
7c68be6
93aeef9
f5f7c35
f697ab9
bad6ec8
d9780c3
05d3a60
81214a6
fc5ec76
6c3943a
4bd3232
bfb1d11
94886a6
fada46f
10b3935
8028c00
4ef8729
0a0e398
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| export function createBezier(params: number[]) { | ||
| const [x1, y1, x2, y2] = params; | ||
| // Pre-calculate polynomial coefficients | ||
| // 3*x1, 3*(x2-x1)-cx, 1-cx-bx, etc. | ||
| const cx = 3 * x1; | ||
| const bx = 3 * (x2 - x1) - cx; | ||
| const ax = 1 - cx - bx; | ||
|
|
||
| const cy = 3 * y1; | ||
| const by = 3 * (y2 - y1) - cy; | ||
| const ay = 1 - cy - by; | ||
|
|
||
| // Calculate x at time t | ||
| function sampleCurveX(t: number) { | ||
| return ((ax * t + bx) * t + cx) * t; | ||
| } | ||
|
|
||
| // Calculate y at time t | ||
| function sampleCurveY(t: number) { | ||
| return ((ay * t + by) * t + cy) * t; | ||
| } | ||
|
|
||
| // Calculate slope (derivative) of x at time t | ||
| function sampleCurveDerivativeX(t: number) { | ||
| return (3 * ax * t + 2 * bx) * t + cx; | ||
| } | ||
|
|
||
| // Solve for t given x (using Newton-Raphson) | ||
| function solveCurveX(x: number) { | ||
| let t2 = x; | ||
| // Iteratively approximate t | ||
| for (let i = 0; i < 8; i++) { | ||
| const x2 = sampleCurveX(t2) - x; | ||
| if (Math.abs(x2) < 1e-6) return t2; | ||
| const d2 = sampleCurveDerivativeX(t2); | ||
| if (Math.abs(d2) < 1e-6) break; | ||
| t2 = t2 - x2 / d2; | ||
| } | ||
| return t2; | ||
| } | ||
|
|
||
| return (x: number) => { | ||
| if (x <= 0) return 0; // Clamp start | ||
| if (x >= 1) return 1; // Clamp end | ||
| const t = solveCurveX(x); | ||
| return sampleCurveY(t); | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import * as d from 'typegpu/data'; | ||
| import tgpu, { type TgpuRoot } from 'typegpu'; | ||
| import { colors } from './geometry.ts'; | ||
| import { createInstanceInfoArray, InstanceInfoArray } from './instanceInfo.ts'; | ||
| import { getGridParams, INITIAL_MIDDLE_SQUARE_SCALE, INITIAL_STEP_ROTATION } from './params.ts'; | ||
|
|
||
| const stepRotationAccess = tgpu.accessor(d.f32); | ||
| const shiftedColorsAccess = tgpu.accessor(d.arrayOf(d.vec4f, 3)); | ||
| const animationProgressAccess = tgpu.accessor(d.f32); | ||
| const middleSquareScaleAccess = tgpu.accessor(d.f32); | ||
| const drawOverNeighborsAccess = tgpu.accessor(d.u32); | ||
| const scaleAccess = tgpu.accessor(d.f32); | ||
| const aspectRatioAccess = tgpu.accessor(d.f32); | ||
|
|
||
| const instanceInfoLayout = tgpu.bindGroupLayout({ | ||
| instanceInfo: { storage: InstanceInfoArray }, | ||
| }); | ||
|
|
||
| function createBuffers(root: TgpuRoot) { | ||
| const animationProgressUniform = root.createUniform(d.f32); | ||
|
|
||
| const shiftedColorsUniform = root.createUniform(d.arrayOf(d.vec4f, 3), colors); | ||
|
|
||
| let instanceInfoBindGroup = createInstanceInfoBufferAndBindGroup(); | ||
|
|
||
| function getInstanceInfoBindGroup() { | ||
| return instanceInfoBindGroup; | ||
| } | ||
|
|
||
| function createInstanceInfoBufferAndBindGroup() { | ||
| const instanceInfoReadonly = root.createReadonly( | ||
| InstanceInfoArray(getGridParams().triangleCount), | ||
| createInstanceInfoArray(), | ||
| ); | ||
|
|
||
| const instanceInfoBindGroup = root.createBindGroup(instanceInfoLayout, { | ||
| instanceInfo: instanceInfoReadonly.buffer, | ||
| }); | ||
|
|
||
| return instanceInfoBindGroup; | ||
| } | ||
|
|
||
| function updateInstanceInfoBufferAndBindGroup() { | ||
| instanceInfoBindGroup = createInstanceInfoBufferAndBindGroup(); | ||
| } | ||
|
|
||
| const scaleUniform = root.createUniform(d.f32, getGridParams().tileDensity); | ||
| const aspectRatioUniform = root.createUniform(d.f32, 1); | ||
|
|
||
| const stepRotationUniform = root.createUniform(d.f32, INITIAL_STEP_ROTATION); | ||
|
|
||
| const middleSquareScaleUniform = root.createUniform(d.f32, INITIAL_MIDDLE_SQUARE_SCALE); | ||
|
|
||
| const drawOverNeighborsUniform = root.createUniform(d.u32, 0); | ||
|
|
||
| return { | ||
| animationProgressUniform, | ||
| aspectRatioUniform, | ||
| drawOverNeighborsUniform, | ||
| getInstanceInfoBindGroup, | ||
| middleSquareScaleUniform, | ||
| scaleUniform, | ||
| shiftedColorsUniform, | ||
| stepRotationUniform, | ||
| updateInstanceInfoBufferAndBindGroup, | ||
| }; | ||
| } | ||
|
|
||
| export { | ||
| animationProgressAccess, | ||
| aspectRatioAccess, | ||
| createBuffers, | ||
| drawOverNeighborsAccess, | ||
| instanceInfoLayout, | ||
| middleSquareScaleAccess, | ||
| scaleAccess, | ||
| shiftedColorsAccess, | ||
| stepRotationAccess, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import * as d from 'typegpu/data'; | ||
| import * as std from 'typegpu/std'; | ||
| import tgpu from 'typegpu'; | ||
|
|
||
| const green = d.vec4f(0.117, 0.839, 0.513, 1); | ||
| const yellow = d.vec4f(0.839, 0.647, 0.117, 1); | ||
| const indigo = d.vec4f(0.38, 0.333, 0.96, 1); | ||
|
|
||
| const colors = [green, yellow, indigo]; | ||
|
|
||
| const originalVertices = tgpu.const(d.arrayOf(d.vec2f, 3), [ | ||
| d.vec2f(std.sqrt(3) / 2, -0.5), | ||
| d.vec2f(0, 1), | ||
| d.vec2f(-std.sqrt(3) / 2, -0.5), | ||
| ]); | ||
|
|
||
| const BASE_TRIANGLE_HEIGHT = 3 / 2; | ||
| const BASE_TRIANGLE_CENTROID_TO_MIDPOINT_LENGTH = 0.5; | ||
| const BASE_TRIANGLE_HALF_SIDE = std.sqrt(3) * 0.5; | ||
|
|
||
| /** | ||
| * Scale factor that would make the base triangle fill | ||
| * the full clip space when `tileDensity` is 1. | ||
| * | ||
| * Derivation: | ||
| * - Original triangle takes 3/4 of the clip space's extent: | ||
| * `(CLIP_SPACE_EXTENT - BASE_TRIANGLE_HEIGHT) / CLIP_SPACE_EXTENT` | ||
| * - `(2 - 1.5) / 2 = 0.5 / 2 = 3/4` | ||
| * - So we need to scale it by the inverse of it, which is `4/3`, to reach full height. | ||
| */ | ||
| const MAGIC_NUMBER = 4 / 3; | ||
|
|
||
| export { | ||
| BASE_TRIANGLE_CENTROID_TO_MIDPOINT_LENGTH, | ||
| BASE_TRIANGLE_HALF_SIDE, | ||
| BASE_TRIANGLE_HEIGHT, | ||
| colors, | ||
| MAGIC_NUMBER, | ||
| originalVertices, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| <canvas data-fit-to-container></canvas> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { colors } from './geometry.ts'; | ||
| import { createBezier } from './bezier.ts'; | ||
| import { | ||
| foregroundFragment, | ||
| foregroundVertex, | ||
| midgroundFragment, | ||
| midgroundVertex, | ||
| } from './shaderModules.ts'; | ||
| import { | ||
| getAnimationDuration, | ||
| getCubicBezierControlPoints, | ||
| getCubicBezierControlPointsString, | ||
| getGridParams, | ||
| INIT_TILE_DENSITY, | ||
| INITIAL_STEP_ROTATION, | ||
| parseControlPoints, | ||
| ROTATION_OPTIONS, | ||
| updateAnimationDuration, | ||
| updateAspectRatio, | ||
| updateCubicBezierControlPoints, | ||
| updateGridParams, | ||
| updateStepRotation, | ||
| } from './params.ts'; | ||
| import { | ||
| animationProgressAccess, | ||
| aspectRatioAccess, | ||
| createBuffers, | ||
| drawOverNeighborsAccess, | ||
| middleSquareScaleAccess, | ||
| scaleAccess, | ||
| shiftedColorsAccess, | ||
| stepRotationAccess, | ||
| } from './buffers.ts'; | ||
| import tgpu from 'typegpu'; | ||
|
|
||
| const canvas = document.querySelector('canvas') as HTMLCanvasElement; | ||
| let ease = createBezier(getCubicBezierControlPoints()); | ||
|
|
||
| export const root = await tgpu.init(); | ||
|
|
||
| const { | ||
| aspectRatioUniform, | ||
| animationProgressUniform, | ||
| drawOverNeighborsUniform, | ||
| getInstanceInfoBindGroup, | ||
| scaleUniform, | ||
| shiftedColorsUniform, | ||
| middleSquareScaleUniform, | ||
| updateInstanceInfoBufferAndBindGroup, | ||
| stepRotationUniform, | ||
| } = createBuffers(root); | ||
|
|
||
| updateAspectRatio(canvas.width, canvas.height, aspectRatioUniform); | ||
|
|
||
| const context = root.configureContext({ canvas, alphaMode: 'premultiplied' }); | ||
|
|
||
| const pipelineBase = root | ||
| .with(animationProgressAccess, animationProgressUniform) | ||
| .with(stepRotationAccess, stepRotationUniform) | ||
| .with(drawOverNeighborsAccess, drawOverNeighborsUniform) | ||
| .with(shiftedColorsAccess, shiftedColorsUniform) | ||
| .with(middleSquareScaleAccess, middleSquareScaleUniform) | ||
| .with(scaleAccess, scaleUniform) | ||
| .with(aspectRatioAccess, aspectRatioUniform); | ||
|
|
||
| const midgroundPipeline = pipelineBase.createRenderPipeline({ | ||
| vertex: midgroundVertex, | ||
| fragment: midgroundFragment, | ||
| }); | ||
|
|
||
| const foregroundPipeline = pipelineBase.createRenderPipeline({ | ||
| vertex: foregroundVertex, | ||
| fragment: foregroundFragment, | ||
| }); | ||
|
|
||
| function getShiftedColors(timestamp: number) { | ||
| const shiftBy = Math.floor(timestamp / getAnimationDuration()) % colors.length; | ||
| return [...colors.slice(shiftBy), ...colors.slice(0, shiftBy)]; | ||
| } | ||
|
|
||
| let animationFrame: number; | ||
|
|
||
| function draw(timestamp: number) { | ||
| const shiftedColors = getShiftedColors(timestamp); | ||
|
|
||
| shiftedColorsUniform.write(shiftedColors); | ||
| animationProgressUniform.write( | ||
| ease((timestamp % getAnimationDuration()) / getAnimationDuration()), | ||
| ); | ||
|
|
||
| const view = context.getCurrentTexture().createView(); | ||
|
|
||
| midgroundPipeline | ||
| .withColorAttachment({ | ||
| view, | ||
| loadOp: 'clear', | ||
| storeOp: 'store', | ||
| clearValue: shiftedColors[0], | ||
| }) | ||
| .with(getInstanceInfoBindGroup()) | ||
| .draw(3, getGridParams().triangleCount); | ||
|
|
||
| foregroundPipeline | ||
| .withColorAttachment({ | ||
| view, | ||
| loadOp: 'load', | ||
| storeOp: 'store', | ||
| }) | ||
| .with(getInstanceInfoBindGroup()) | ||
| .draw(3, getGridParams().triangleCount); | ||
|
|
||
| animationFrame = requestAnimationFrame(draw); | ||
| } | ||
|
|
||
| animationFrame = requestAnimationFrame(draw); | ||
|
|
||
| // cleanup | ||
| const resizeObserver = new ResizeObserver(() => { | ||
| updateAspectRatio(canvas.width, canvas.height, aspectRatioUniform); | ||
| updateGridParams(scaleUniform, updateInstanceInfoBufferAndBindGroup); | ||
| updateInstanceInfoBufferAndBindGroup(); | ||
| }); | ||
|
|
||
| resizeObserver.observe(canvas); | ||
|
|
||
| export function onCleanup() { | ||
| cancelAnimationFrame(animationFrame); | ||
| resizeObserver.disconnect(); | ||
| root.destroy(); | ||
| } | ||
|
|
||
| // Example controls | ||
|
|
||
| export const controls = { | ||
| 'Tile density': { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In examples we usually start controls' names with lowercase letters (exception: buttons)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aleksanderkatan |
||
| initial: INIT_TILE_DENSITY, | ||
| min: 0.01, | ||
| max: 1.33, | ||
| step: 0.01, | ||
| onSliderChange: (newValue: number) => | ||
| updateGridParams(scaleUniform, updateInstanceInfoBufferAndBindGroup, newValue), | ||
| }, | ||
| 'Animation duration': { | ||
| initial: getAnimationDuration(), | ||
| min: 250, | ||
| max: 3500, | ||
| step: 25, | ||
| onSliderChange: updateAnimationDuration, | ||
| }, | ||
| 'Rotation in degrees': { | ||
| initial: INITIAL_STEP_ROTATION, | ||
| options: ROTATION_OPTIONS, | ||
| onSelectChange: (newValue: number) => | ||
| updateStepRotation(newValue, stepRotationUniform, middleSquareScaleUniform), | ||
| }, | ||
| 'Draw over neighbors': { | ||
| initial: false, | ||
| onToggleChange(value: boolean) { | ||
| drawOverNeighborsUniform.write(value ? 1 : 0); | ||
| }, | ||
| }, | ||
| 'Cubic Bezier Control Points': { | ||
| initial: getCubicBezierControlPointsString(), | ||
| async onTextChange(value: string) { | ||
| const newPoints = parseControlPoints(value); | ||
|
|
||
| updateCubicBezierControlPoints(newPoints); | ||
| ease = createBezier(newPoints); | ||
| }, | ||
| }, | ||
| 'Edit Cubic Bezier Points': { | ||
| onButtonClick: () => { | ||
| window.open(`https://cubic-bezier.com/?#${getCubicBezierControlPoints().join()}`, '_blank'); | ||
| }, | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import * as d from 'typegpu/data'; | ||
| import { getGridParams } from './params.ts'; | ||
| import { | ||
| BASE_TRIANGLE_CENTROID_TO_MIDPOINT_LENGTH, | ||
| BASE_TRIANGLE_HALF_SIDE, | ||
| BASE_TRIANGLE_HEIGHT, | ||
| } from './geometry.ts'; | ||
|
|
||
| const InstanceInfo = d.struct({ offset: d.vec2f, rotationAngle: d.f32 }); | ||
| const InstanceInfoArray = d.arrayOf(InstanceInfo); | ||
|
|
||
| function createInstanceInfoArray() { | ||
| const instanceInfoArray = Array.from({ length: getGridParams().triangleCount }, (_, index) => { | ||
| const row = Math.floor(index / getGridParams().trianglesPerRow); | ||
| const column = index % getGridParams().trianglesPerRow; | ||
|
|
||
| let info: d.Infer<typeof InstanceInfo>; | ||
|
|
||
| const offsetX = (column - 1) * BASE_TRIANGLE_HALF_SIDE * getGridParams().tileDensity; | ||
|
|
||
| if (column % 2 === 1) { | ||
| info = { | ||
| offset: d.vec2f( | ||
| offsetX, | ||
| BASE_TRIANGLE_CENTROID_TO_MIDPOINT_LENGTH * getGridParams().tileDensity, | ||
| ), | ||
| rotationAngle: 60, | ||
| }; | ||
| } else { | ||
| info = { | ||
| offset: d.vec2f(offsetX, 0), | ||
| rotationAngle: 0, | ||
| }; | ||
| } | ||
|
|
||
| info.offset.y += -row * BASE_TRIANGLE_HEIGHT * getGridParams().tileDensity; | ||
| // hide empty pixel lines | ||
| info.offset.y *= 0.9999; | ||
|
|
||
| return info; | ||
| }); | ||
|
|
||
| return instanceInfoArray; | ||
| } | ||
|
|
||
| export { createInstanceInfoArray, InstanceInfo, InstanceInfoArray }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes are currently on the main branch, thus delaying the example showing up until the next release. You can rebase onto release if you wish to see it sooner!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aleksanderkatan thx for letting me know. Did just that.