Skip to content

Commit

Permalink
feat: lesson 8 use depth render target
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver committed May 11, 2024
1 parent 9429246 commit c8facf0
Show file tree
Hide file tree
Showing 56 changed files with 1,678 additions and 380 deletions.
19 changes: 19 additions & 0 deletions packages/lesson_008/examples/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<html>
<script src="https://unpkg.com/[email protected]/touch-emulator.js"></script>
<script type="module" src="./main.ts"></script>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}

#canvas {
touch-action: none;
}
</style>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
59 changes: 59 additions & 0 deletions packages/lesson_008/examples/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Canvas, Circle } from '../src';

const $canvas = document.getElementById('canvas') as HTMLCanvasElement;
const resize = (width: number, height: number) => {
$canvas.width = width * window.devicePixelRatio;
$canvas.height = height * window.devicePixelRatio;
$canvas.style.width = `${width}px`;
$canvas.style.height = `${height}px`;
$canvas.style.outline = 'none';
$canvas.style.padding = '0px';
$canvas.style.margin = '0px';
};
resize(window.innerWidth, window.innerHeight);

const canvas = await new Canvas({
canvas: $canvas,
// renderer: 'webgpu',
shaderCompilerPath:
'https://unpkg.com/@antv/[email protected]/dist/pkg/glsl_wgsl_compiler_bg.wasm',
}).initialized;

const circles: Circle[] = [];
for (let i = 0; i < 100; i++) {
const fill = `rgb(${Math.floor(Math.random() * 255)},${Math.floor(
Math.random() * 255,
)},${Math.floor(Math.random() * 255)})`;
const circle = new Circle({
cx: Math.random() * 400,
cy: Math.random() * 200,
r: Math.random() * 20,
// cx: i * 50,
// cy: i * 50,
// r: 100,
fill,
stroke: 'black',
strokeWidth: 2,
batchable: true,
});
canvas.appendChild(circle);
circles.push(circle);

circle.addEventListener('pointerenter', () => {
circle.fill = 'red';
});
circle.addEventListener('pointerleave', () => {
circle.fill = fill;
});
}

const animate = () => {
canvas.render();
requestAnimationFrame(animate);
};
animate();

window.addEventListener('resize', () => {
resize(window.innerWidth, window.innerHeight);
canvas.resize(window.innerWidth, window.innerHeight);
});
2 changes: 1 addition & 1 deletion packages/lesson_008/src/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export class Canvas {
}

removeChild(shape: Shape) {
this.#root.removeChild(shape);
return this.#root.removeChild(shape);
}

setCheckboardStyle(style: CheckboardStyle) {
Expand Down
144 changes: 144 additions & 0 deletions packages/lesson_008/src/drawcalls/BatchManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Buffer, Device, RenderPass } from '@antv/g-device-api';
import { Drawcall, SDF } from '../drawcalls';
import { Circle, type Shape } from '../shapes';

/**
* Since a shape may have multiple drawcalls, we need to cache them and maintain an 1-to-many relationship.
*
* e.g. we need 2 drawcalls for a Circle with dashed stroke:
* - A SDF drawcall to draw the fill.
* - A Path drawcall to draw the dashed stroke.
*/
const SHAPE_DRAWCALL_CTORS = new WeakMap<typeof Shape, (typeof Drawcall)[]>();
SHAPE_DRAWCALL_CTORS.set(Circle, [SDF]);

export class BatchManager {
/**
* Drawcalls to flush in the next frame.
*/
#drawcallsToFlush: Drawcall[] = [];

/**
* Cache drawcalls for non batchable shape.
*/
#nonBatchableDrawcallsCache: Record<number, Drawcall[]> = Object.create(null);

#batchableDrawcallsCache: Record<number, Drawcall[]> = Object.create(null);

#instancesCache = new WeakMap<typeof Shape, Drawcall[][]>();

constructor(private device: Device) {}

private createDrawcalls(shape: Shape, instanced = false) {
return SHAPE_DRAWCALL_CTORS.get(shape.constructor as typeof Shape)?.map(
(DrawcallCtor) => {
// @ts-ignore
const drawcall = new DrawcallCtor(this.device, instanced);
drawcall.add(shape);
return drawcall;
},
);
}

private getOrCreateNonBatchableDrawcalls(shape: Shape) {
let existed = this.#nonBatchableDrawcallsCache[shape.uid];
if (!existed) {
existed = this.createDrawcalls(shape) || [];
this.#nonBatchableDrawcallsCache[shape.uid] = existed;
}

return existed;
}

private getOrCreateBatchableDrawcalls(shape: Shape) {
let existed: Drawcall[] | undefined =
this.#batchableDrawcallsCache[shape.uid];
if (!existed) {
const shapeCtor = shape.constructor as typeof Shape;
let instancedDrawcalls = this.#instancesCache.get(shapeCtor);
if (!instancedDrawcalls) {
instancedDrawcalls = [];
}

existed = instancedDrawcalls.find((drawcalls) =>
drawcalls.every((drawcall) => drawcall.validate()),
);

if (!existed) {
existed = this.createDrawcalls(shape, true) || [];
instancedDrawcalls.push(existed);
this.#instancesCache.set(shapeCtor, instancedDrawcalls);
}

existed.forEach((drawcall) => {
drawcall.add(shape);
});

this.#batchableDrawcallsCache[shape.uid] = existed;
}

return existed;
}

add(shape: Shape) {
if (shape.batchable) {
const drawcalls = this.getOrCreateBatchableDrawcalls(shape);
if (this.#drawcallsToFlush.indexOf(drawcalls[0]) === -1) {
this.#drawcallsToFlush.push(...drawcalls);
}
} else {
this.#drawcallsToFlush.push(
...this.getOrCreateNonBatchableDrawcalls(shape),
);
}
}

/**
* Called when a shape is:
* * removed from the scene graph.
* * culled from viewport.
* * invisible.
*/
remove(shape: Shape) {
if (shape.batchable) {
this.getOrCreateBatchableDrawcalls(shape).forEach((drawcall) => {
drawcall.remove(shape);
});
delete this.#batchableDrawcallsCache[shape.uid];
} else {
this.getOrCreateNonBatchableDrawcalls(shape).forEach((drawcall) => {
drawcall.destroy();
});
delete this.#nonBatchableDrawcallsCache[shape.uid];
}
}

destroy() {
for (const key in this.#nonBatchableDrawcallsCache) {
this.#nonBatchableDrawcallsCache[key].forEach((drawcall) => {
drawcall.destroy();
});
}
for (const key in this.#batchableDrawcallsCache) {
this.#batchableDrawcallsCache[key].forEach((drawcall) => {
drawcall.destroy();
});
}
}

clear() {
this.#drawcallsToFlush = [];
}

flush(renderPass: RenderPass, uniformBuffer: Buffer) {
this.#drawcallsToFlush.forEach((drawcall) => {
drawcall.submit(renderPass, uniformBuffer);
});
}

stats() {
return {
drawcallCount: this.#drawcallsToFlush.length,
};
}
}
71 changes: 43 additions & 28 deletions packages/lesson_008/src/drawcalls/Drawcall.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,66 @@
import { Buffer, Device, Program, RenderPass } from '@antv/g-device-api';
import { Buffer, Device, RenderPass } from '@antv/g-device-api';
import { Shape } from '../shapes';

export class Drawcall {
shapes: Shape[] = [];
get instance() {
return this.shapes[0];
}

instanced: boolean;
export const ZINDEX_FACTOR = 50000;

protected program: Program;
export abstract class Drawcall {
protected shapes: Shape[] = [];

/**
* Create a new batch if the number of instances exceeds.
*/
maxInstances = Infinity;
protected maxInstances = Infinity;

index = 0;
protected geometryDirty = true;
protected materialDirty = true;

geometryDirty = true;
constructor(protected device: Device, protected instanced: boolean) {}

constructor(protected device: Device) {}
abstract createGeometry(): void;
abstract createMaterial(uniformBuffer: Buffer): void;
abstract render(renderPass: RenderPass): void;
abstract destroy(): void;

validate() {
return this.count() <= this.maxInstances - 1;
}

shouldMerge(shape: Shape, index: number) {
if (!this.instance) {
return true;
submit(renderPass: RenderPass, uniformBuffer: Buffer) {
if (this.geometryDirty) {
this.createGeometry();
}

if (this.instance.constructor !== shape.constructor) {
return false;
if (this.materialDirty) {
this.createMaterial(uniformBuffer);
}

return true;
}
this.render(renderPass);

protected init() {}
protected render(renderPass: RenderPass, uniformBuffer: Buffer) {}
if (this.geometryDirty) {
this.geometryDirty = false;
}

submit(renderPass: RenderPass, uniformBuffer: Buffer) {
if (!this.program) {
this.init();
if (this.materialDirty) {
this.materialDirty = false;
}
}

add(shape: Shape) {
if (!this.shapes.includes(shape)) {
this.shapes.push(shape);
this.geometryDirty = true;
}
}

this.render(renderPass, uniformBuffer);
remove(shape: Shape) {
if (this.shapes.includes(shape)) {
const index = this.shapes.indexOf(shape);
this.shapes.splice(index, 1);
this.geometryDirty = true;
}
}

destroy() {
this.program.destroy();
count() {
return this.shapes.length;
}
}
Loading

0 comments on commit c8facf0

Please sign in to comment.