Sketchy is a simple drawing program that draws strokes in response to mouse or touch events. As each new input event is processed, a path is fit to the input, and used to generate a triangle mesh for the stroke. The wobbly stroke outlines are animated by a vertex shader.
The path of a stroke is represented by a sequence of cubic Bezier curves. The stroke-fitting algorithm begins by fitting a Bezier curve to the list of input points, and measuring the error of the resulting curve (i.e. how far the input points are from the approximating curve.
If the error metric is below a threshold, then the stroke path consists of a single cubic Bezier curve.
More commonly, a stroke cannot be adequately represented by a single Bezier curve, and so the procedure above is modified slightly so that it can be applied recursively: instead of fitting a Bezier curve to the entire list, it is fit to a sub-list defined by a start and end index:
A stroke is rendered by generating a triangle mesh from the Bezier path that was fit to the stroke's input points. This mesh can be reused to render multiple frames if the stroke has not changed since the last frame.
The mesh is generated by:
In order to choose the parameter values at which to evaluate each Bezier segment, and arc-length reparameterization is first computed for the segment. This allows parameters to be chosen such that adjacent vertices are approximately equidistant.
The wobbly outlines of a Sketchy stroke are computed each frame in the vertex shader (discussed below). In addition to the vertex positions computed above, this shader requires other vertex attributes to be baked into the mesh. To support this, the mesh has the following attributes:
MeshAttribute::kPosition2D
MeshAttribute::kPositionOffset
MeshAttribute::kPerimeterPos
The “wobbly outline” effect is hard-coded into Escher and is specified by adding a ModifierWobble
to the Shape that encapsulates the stroke mesh. This is used to select the Vulkan pipeline object that is used to render the stroke (see escher/impl/model_pipeline_cache.cc
), and to provide per-stroke parameters for that pipeline.
The effect is implemented by moving each vertex by some multiple of the vector specified by its kPositionOffset
attribute. To compute this multiple, the vertex shader uses the ModiferWobble::SineParams
associated with the stroke, as well as the kPerimeterPos
attribute of the vertex.
The stroke-fitting algorithm is applied naively; as each new input point arrives, the entire path is refit. This has two negative consequences:
The obvious solution is to not refit the entire list of input points, but instead to reuse some of the Bezier segments that were computed the last time that the stroke was extended. When the stroke is next extended, the recursive stroke-fitting procedure is initiated with a start-index that skips input points corresponding to the reused Bezier segments.
Stroke vertices are currently generated on the CPU. An alternative would be to generate them on the GPU, as seen in strokeBezierTesslate()
in https://github.com/schwa423/Sketchy/blob/qi/qi/pen/port_ios/pen.metal.
sketchy_demo
is currently a standalone application that renders strokes directly into a framebuffer, including shadows cast by each stroke. This is undesirable; Mozart clients should not directly render their own shadows due to “double-shadowing” artifacts when the Mozart SceneManager generates its own shadows based on the geometry of shapes in the global scene-graph.
Instead, Sketchy should provide meshes that are rendered by the SceneManager. This allows strokes to cast shadows onto other SceneManager geometry, and also to have shadows cast onto them. Depending on the elevations of each stroke, another SceneManager client might add a Shape that casts a shadow on one stroke, and is shadowed by another!!
One obstacle is that the SceneManager will not expose ShapeModifier::kWobble
as part of its public API (see mozart/services/scene/*
); it not generally useful to non-Sketchy clients.
Sketchy can overcome this obstacle by using the GPU to generate new vertices each frame. The SceneManager supports this use-case via the Buffer
and MeshShape
resources (see resources.fidl
and shapes.fidl
). This could be done by using the vertex shader to write new vertices into an output buffer instead of passing them along to the rasterizer, or even by completely re-tessellating the stroke each frame; the latter approach would allow the entire stroke path to be animated, not just the stroke outline.