Outline rendering WIP

This commit is contained in:
Jamie Wong
2018-01-29 11:53:39 -08:00
parent 45dd408163
commit 838cc06ff5
7 changed files with 457 additions and 35 deletions
+8 -1
View File
@@ -5,6 +5,7 @@ import { TextureCachedRenderer, TextureRenderer, TextureRendererProps } from './
import { StatsPanel } from './stats'
import { Vec2, Rect } from './math';
import { OutlineRenderer, OutlineRendererProps } from './outline-renderer';
type FrameCallback = () => void
@@ -17,6 +18,7 @@ export class CanvasContext {
private rectangleBatchRenderer: RectangleBatchRenderer
private viewportRectangleRenderer: ViewportRectangleRenderer
private textureRenderer: TextureRenderer
private outlineRenderer: OutlineRenderer
private setViewportScope: regl.Command<{ physicalBounds: Rect }>
private setScissor: regl.Command<{}>
@@ -26,7 +28,7 @@ export class CanvasContext {
attributes: {
antialias: false
},
extensions: ['ANGLE_instanced_arrays'],
extensions: ['ANGLE_instanced_arrays', 'WEBGL_depth_texture'],
optionalExtensions: ['EXT_disjoint_timer_query'],
profile: true
})
@@ -34,6 +36,7 @@ export class CanvasContext {
this.rectangleBatchRenderer = new RectangleBatchRenderer(this.gl)
this.viewportRectangleRenderer = new ViewportRectangleRenderer(this.gl)
this.textureRenderer = new TextureRenderer(this.gl)
this.outlineRenderer = new OutlineRenderer(this.gl)
this.setScissor = this.gl({ scissor: { enable: true } })
this.setViewportScope = this.gl<SetViewportScopeProps>({
context: {
@@ -122,6 +125,10 @@ export class CanvasContext {
this.textureRenderer.render(props)
}
drawOutlines(props: OutlineRendererProps) {
this.outlineRenderer.render(props)
}
createRectangleBatch(): RectangleBatch {
return new RectangleBatch(this.gl)
}
+79 -23
View File
@@ -112,7 +112,8 @@ class RangeTreeLeafNode implements RangeTreeNode {
constructor(
private batch: RectangleBatch,
private bounds: Rect
private bounds: Rect,
private numPrecedingRectanglesInRow: number
) {
batch.uploadToGPU()
}
@@ -121,6 +122,7 @@ class RangeTreeLeafNode implements RangeTreeNode {
getBounds() { return this.bounds }
getRectCount() { return this.batch.getRectCount() }
getChildren() { return this.children }
getParity() { return this.numPrecedingRectanglesInRow % 2 }
forEachLeafNodeWithinBounds(configSpaceBounds: Rect, cb: (leaf: RangeTreeLeafNode) => void) {
if (!this.bounds.hasIntersectionWith(configSpaceBounds)) return
cb(this)
@@ -178,11 +180,15 @@ interface FlamechartRowAtlasKey {
export class FlamechartRenderer {
private layers: RangeTreeNode[] = []
private rowAtlas: RowAtlas<FlamechartRowAtlasKey>
private colorTexture: regl.Texture
private depthTexture: regl.Texture
private framebuffer: regl.Framebuffer
private renderToFramebuffer: regl.Command<{}>
private withContext: regl.Command<{}>
constructor(private canvasContext: CanvasContext, private flamechart: Flamechart) {
const nLayers = flamechart.getLayers().length
this.rowAtlas = new RowAtlas(canvasContext)
for (let stackDepth = 0; stackDepth < nLayers; stackDepth++) {
const leafNodes: RangeTreeLeafNode[] = []
const y = stackDepth
@@ -191,12 +197,14 @@ export class FlamechartRenderer {
let maxRight = -Infinity
let batch = canvasContext.createRectangleBatch()
let rectCount = 0
for (let frame of flamechart.getLayers()[stackDepth]) {
if (batch.getRectCount() >= MAX_BATCH_SIZE) {
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
new Vec2(minLeft, stackDepth),
new Vec2(maxRight - minLeft, 1)
)))
), rectCount))
minLeft = Infinity
maxRight = -Infinity
batch = canvasContext.createRectangleBatch()
@@ -209,17 +217,33 @@ export class FlamechartRenderer {
maxRight = Math.max(maxRight, configSpaceBounds.right())
const color = flamechart.getColorForFrame(frame.node.frame)
batch.addRect(configSpaceBounds, color)
rectCount++
}
if (batch.getRectCount() > 0) {
leafNodes.push(new RangeTreeLeafNode(batch, new Rect(
new Vec2(minLeft, stackDepth),
new Vec2(maxRight - minLeft, 1)
)))
), rectCount))
}
// TODO(jlfwong): Probably want this to be a binary tree
// TODO(jlfwong): Making this into a binary tree
// range than a tree of always-height-two might make this run faster
this.layers.push(new RangeTreeInteriorNode(leafNodes))
// TODO(jlfwong): Extract this to CanvasContext
this.withContext = canvasContext.gl({})
this.colorTexture = this.canvasContext.gl.texture({ width: 1, height: 1 })
this.depthTexture = this.canvasContext.gl.texture({ width: 1, height: 1, format: 'depth', type: 'uint16' })
this.framebuffer = this.canvasContext.gl.framebuffer({
color: [this.colorTexture],
depth: this.depthTexture
})
this.renderToFramebuffer = canvasContext.gl({
framebuffer: this.framebuffer
})
}
}
@@ -261,7 +285,7 @@ export class FlamechartRenderer {
}
const top = Math.max(0, Math.floor(configSpaceSrcRect.top()))
const bottom = Math.min(this.layers.length - 1, Math.ceil(configSpaceSrcRect.bottom()))
const bottom = Math.min(this.layers.length, Math.ceil(configSpaceSrcRect.bottom()))
const configSpaceContentWidth = this.flamechart.getTotalWeight()
const numAtlasEntriesPerLayer = Math.pow(2, zoomLevel)
@@ -288,29 +312,61 @@ export class FlamechartRenderer {
this.canvasContext.drawRectangleBatch({
batch: leaf.getBatch(),
configSpaceSrcRect: configSpaceBounds,
physicalSpaceDstRect: textureDstRect
physicalSpaceDstRect: textureDstRect,
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
parityOffset: leaf.getParity()
})
})
})
// Render from the cache
for (let key of keysToRenderCached) {
const configSpaceSrcRect = this.configSpaceBoundsForKey(key)
this.rowAtlas.renderViaAtlas(key, configToPhysical.transformRect(configSpaceSrcRect))
}
this.withContext((context: regl.Context) => {
this.framebuffer.resize(context.viewportWidth, context.viewportHeight)
})
// Render entries that didn't make it into the cache
for (let key of keysToRenderUncached) {
const configSpaceBounds = this.configSpaceBoundsForKey(key)
const physicalBounds = configToPhysical.transformRect(configSpaceBounds)
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => {
this.canvasContext.drawRectangleBatch({
batch: leaf.getBatch(),
configSpaceSrcRect,
physicalSpaceDstRect: physicalBounds
this.renderToFramebuffer((context: regl.Context) => {
// this.withContext((context: regl.Context) => {
this.canvasContext.gl.clear({color: [0, 0, 0, 0]})
const viewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))
const configToViewport = AffineTransform.betweenRects(configSpaceSrcRect, viewportRect)
// Render from the cache
for (let key of keysToRenderCached) {
const configSpaceSrcRect = this.configSpaceBoundsForKey(key)
this.rowAtlas.renderViaAtlas(key, configToViewport.transformRect(configSpaceSrcRect))
}
// Render entries that didn't make it into the cache
for (let key of keysToRenderUncached) {
const configSpaceBounds = this.configSpaceBoundsForKey(key)
const physicalBounds = configToViewport.transformRect(configSpaceBounds)
this.layers[key.stackDepth].forEachLeafNodeWithinBounds(configSpaceBounds, (leaf) => {
this.canvasContext.drawRectangleBatch({
batch: leaf.getBatch(),
configSpaceSrcRect,
physicalSpaceDstRect: physicalBounds,
parityMin: key.stackDepth % 2 == 0 ? 2 : 0,
parityOffset: leaf.getParity()
})
})
})
}
}
})
this.canvasContext.drawOutlines({
colorTexture: this.colorTexture,
depthTexture: this.depthTexture,
srcRect: new Rect(Vec2.zero, new Vec2(this.colorTexture.width, this.colorTexture.height)),
dstRect: physicalSpaceDstRect
})
// Paint the color texture for debugging
/*
this.canvasContext.drawTexture({
texture: this.colorTexture,
srcRect: new Rect(Vec2.zero, new Vec2(this.colorTexture.width, this.colorTexture.height)),
dstRect: physicalSpaceDstRect
})
*/
// Overlay the atlas on top of the canvas for debugging
/*
+130
View File
@@ -0,0 +1,130 @@
import * as regl from 'regl'
import { Vec2, Rect, AffineTransform } from './math'
export class OutlineRendererProps {
colorTexture: regl.Texture
depthTexture: regl.Texture
srcRect: Rect
dstRect: Rect
}
export class OutlineRenderer {
private command: regl.Command<OutlineRendererProps>
constructor(gl: regl.Instance) {
this.command = gl({
vert: `
uniform mat3 uvTransform;
uniform mat3 positionTransform;
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = (uvTransform * vec3(uv, 1)).xy;
gl_Position = vec4((positionTransform * vec3(position, 1)).xy, 0, 1);
}
`,
frag: `
precision mediump float;
uniform vec2 uvSpacePixelSize;
varying vec2 vUv;
uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
void main() {
// Sample the 4 surrounding pixels in the depth texture to determine
// if we should draw a boundary here or not.
float N = texture2D(depthTexture, vUv + vec2(0, uvSpacePixelSize.y)).r;
float E = texture2D(depthTexture, vUv + vec2(uvSpacePixelSize.x, 0)).r;
float S = texture2D(depthTexture, vUv + vec2(0, -uvSpacePixelSize.y)).r;
float W = texture2D(depthTexture, vUv + vec2(-uvSpacePixelSize.x, 0)).r;
float here = texture2D(depthTexture, vUv).x;
if (
here == N && here != S || // Top edge
here == S && here != N || // Bottom edge
here == E && here != W || // Left edge
here == W && here != E
) {
// We're on an edge! Draw white.
gl_FragColor = vec4(1, 1, 1, 1);
} else {
gl_FragColor = texture2D(colorTexture, vUv);
}
}
`,
depth: {
enable: false
},
attributes: {
// Cover full canvas with a rectangle
// with 2 triangles using a triangle
// strip.
//
// 0 +--+ 1
// | /|
// |/ |
// 2 +--+ 3
position: gl.buffer([
[-1, 1],
[1, 1],
[-1, -1],
[1, -1]
]),
uv: gl.buffer([
[0, 1],
[1, 1],
[0, 0],
[1, 0]
])
},
count: 4,
primitive: 'triangle strip',
uniforms: {
colorTexture: (context, props) => props.colorTexture,
depthTexture: (context, props) => props.depthTexture,
uvTransform: (context, props) => {
const { srcRect, colorTexture } = props
const physicalToUV = AffineTransform.withTranslation(new Vec2(0, 1))
.times(AffineTransform.withScale(new Vec2(1, -1)))
.times(AffineTransform.betweenRects(
new Rect(Vec2.zero, new Vec2(colorTexture.width, colorTexture.height)),
Rect.unit
))
const uvRect = physicalToUV.transformRect(srcRect)
return AffineTransform.betweenRects(
Rect.unit,
uvRect,
).flatten()
},
uvSpacePixelSize: (context, props) => {
return Vec2.unit.dividedByPointwise(new Vec2(props.colorTexture.width, props.colorTexture.height)).flatten()
},
positionTransform: (context, props) => {
const { dstRect } = props
const viewportSize = new Vec2(context.viewportWidth, context.viewportHeight)
const physicalToNDC = AffineTransform.withScale(new Vec2(1, -1))
.times(AffineTransform.betweenRects(
new Rect(Vec2.zero, viewportSize),
Rect.NDC)
)
const ndcRect = physicalToNDC.transformRect(dstRect)
return AffineTransform.betweenRects(Rect.NDC, ndcRect).flatten()
}
},
})
}
render(props: OutlineRendererProps) {
this.command(props)
}
}
+46 -5
View File
@@ -32,10 +32,21 @@ export class RectangleBatch {
return this.colorBuffer
}
private indexBuffer: regl.Buffer | null = null
getIndexBuffer() {
if (!this.indexBuffer) {
const indices = new Float32Array(this.rectCount)
for (let i = 0; i < this.rectCount; i++) indices[i] = i
this.indexBuffer = this.gl.buffer(indices)
}
return this.indexBuffer
}
uploadToGPU() {
this.getConfigSpaceOffsetBuffer()
this.getConfigSpaceSizeBuffer()
this.getColorBuffer()
this.getIndexBuffer()
}
addRect(rect: Rect, color: Color) {
@@ -71,14 +82,23 @@ export interface RectangleBatchRendererProps {
batch: RectangleBatch
configSpaceSrcRect: Rect
physicalSpaceDstRect: Rect
parityMin?: number
parityOffset?: number
}
export class RectangleBatchRenderer {
private command: regl.Command<RectangleBatchRendererProps>
constructor(gl: regl.Instance) {
// We draw the parity / 4 into the depth channel so it
// can be used in a post-processing step to draw boundaries
// between rectangles. We use 4 different values (2 per row)
// so we can distingish both between adjacent rectangles on a row
// and between rows!
this.command = gl({
vert: `
uniform mat3 configSpaceToNDC;
uniform float parityMin;
uniform float parityOffset;
// Non-instanced
attribute vec2 corner;
@@ -87,23 +107,27 @@ export class RectangleBatchRenderer {
attribute vec2 configSpaceOffset;
attribute vec2 configSpaceSize;
attribute vec3 color;
attribute float index;
varying vec3 vColor;
void main() {
vColor = color;
float depth = parityMin + mod(parityOffset + index, 2.0);
vec2 configSpacePos = configSpaceOffset + corner * configSpaceSize;
vec2 position = (configSpaceToNDC * vec3(configSpacePos, 1)).xy;
gl_Position = vec4(position, 0, 1);
gl_Position = vec4(position, depth / 4.0, 1);
}
`,
depth: {
enable: false
},
depth: {
enable: false
},
frag: `
frag: `
precision mediump float;
varying vec3 vColor;
varying float vParity;
void main() {
gl_FragColor = vec4(vColor, 1);
}
@@ -145,6 +169,15 @@ export class RectangleBatchRenderer {
size: 3,
divisor: 1
}
},
index: (context, props) => {
return {
buffer: props.batch.getIndexBuffer(),
offset: 0,
stride: 4,
size: 1,
divisor: 1
}
}
},
@@ -161,6 +194,14 @@ export class RectangleBatchRenderer {
.times(AffineTransform.withScale(new Vec2(2, -2).dividedByPointwise(viewportSize)))
return physicalToNDC.times(configToPhysical).flatten()
},
parityOffset: (context, props) => {
return props.parityOffset || 0
},
parityMin: (context, props) => {
return props.parityMin || 0
}
},
Vendored
+7 -6
View File
@@ -222,6 +222,7 @@ declare module "regl" {
height: number
(args: TextureArgs): void
resize(width: number, height: number): void
destroy(): void
stats: {
size: number
@@ -259,6 +260,11 @@ declare module "regl" {
colorType?: 'uint8' | 'half float' | 'float'
}
interface Framebuffer {
width: number
height: number
color?: RenderBuffer[]
depth?: RenderBuffer
(options: FramebufferOptions): void
resize(width: number, height: number): void
destroy(): void
@@ -414,14 +420,9 @@ declare module "regl" {
(p: P, cb: (context: Context) => void): void
stats: {
/** Aggregate time spent on the GPU in ms */
gpuTime: number
/** Aggregate time spent on the CPU in ms */
cpuTime: number
/** Total number of calls of this command */
count: number
hitCount: number
}
}
}
+182
View File
@@ -0,0 +1,182 @@
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
a;b 1
c;d 1
+5
View File
@@ -142,6 +142,7 @@ export class TextureCachedRenderer<T> {
this.withContext((context: regl.Context) => {
let needsRender = false
if (this.texture.width !== context.viewportWidth || this.texture.height !== context.viewportHeight) {
// TODO(jlfwong): Can probably just use this.framebuffer.resize
this.texture({ width: context.viewportWidth, height: context.viewportHeight })
this.framebuffer({ color: [this.texture] })
needsRender = true
@@ -155,6 +156,9 @@ export class TextureCachedRenderer<T> {
if (needsRender) {
// Render to texture
// TODO(jlfwong): Re-enable this when I figure out how to
// resize a framebuffer while another framebuffer is active.
/*
this.gl({
viewport: (context, props) => {
return {
@@ -169,6 +173,7 @@ export class TextureCachedRenderer<T> {
this.gl.clear({color: [0, 0, 0, 0]})
this.renderUncached(props)
})
*/
}
const glViewportRect = new Rect(Vec2.zero, new Vec2(context.viewportWidth, context.viewportHeight))