Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
176 changes: 176 additions & 0 deletions apps/typegpu-docs/src/examples/simple/rotating-triangle-tiles/index.ts
Copy link
Copy Markdown
Contributor

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!

Copy link
Copy Markdown
Collaborator Author

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.

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': {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In examples we usually start controls' names with lowercase letters (exception: buttons)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aleksanderkatan
I don't know about that. I've seen many where capitalized names are the norm. Like jump-flood-voronoi, jump-flood-distance, jelly-slider to name a few.
Imo, it's better to handle it at the level of Controls themselves at this point. Just make the names there capitalized or not if consistency in control names is important. At this point, it's a mix of both. Hard to say which one is a standard.

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 };
Loading
Loading