-
Notifications
You must be signed in to change notification settings - Fork 336
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
Subpixel SVG costume handling: a writeup #550
Comments
Thank you for this really thorough documentation and all this research! It really helps put all the PRs into context. |
This was referenced May 5, 2020
Closed
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The Problem
When editing a vector sprite, weird errors appear. Sometimes, columns/rows appear doubled up, and the very edges of a sprite may get cut off.
This becomes particularly noticeable if you're using vector sprites in an animation, and you draw several different frames:
Notice how unstable this appears-- the cat is jittering around, and sometimes the very bottom of its feet get cut off.
The Root Cause
This is due to a fundamental difference between vector and raster graphics: subpixels.
An SVG costume's
viewBox
, which describes its bounds, can have non-integer dimensions. You can, for instance, have a sprite that's 2.5 pixels wide. However, eventually the sprite will need to be rendered to a texture, and textures necessarily have integer dimensions.That's not the only problem, however. To further complicate matters, costumes can have non-integer rotation centers. These rotation centers shift a sprite over by a sub-pixel amount. Consider this SVG of a 3x3 circle (viewbox in red):
Let's say we rasterize this circle as-is:
This appears fine. However, the center of this circle is at (1.5, 1.5). This will cause problems if we want to render the circle in the center of a, say, 6x6 stage:
Now, we need to do some resampling of the already-rasterized SVG! If we do nearest-neighbor resampling, we get the "jaggies" shown earlier. If we do linear resampling, the SVG appears quite blurry. Clearly, we need to take care of this before rasterization.
The Theory
What we need to do is:
viewBox
es, although it's not shown here:scratch-render
so that it is relative to the new bounding box. For instance, the above circle's rotation center is now (2, 2).The Practice (Attempt 1)
Now comes the implementation. We modify
SVGRenderer.loadSVG
to take the costume's intended rotation center as an argument, and before creating the SVG image, apply the necessary transformations to itsviewBox
. Then, we offset the rotation center used byscratch-render
by adding it toSVGRenderer.viewOffset
.Unfortunately, this doesn't work with mipmaps.
If you render the costume at above 100% size, it works fine, but if you render a mipmap under the costume's "native" size, then the rasterized costume may once again have a non-integer size or rotation center--for instance, if you rasterize at half the costume's normal size, you're dividing the rotation center and dimensions by 2.
The Practice (Attempt 2)
What if we instead applied this bounding-box adjustment each time the costume was rasterized?
SVGSkin
would pass the costume's rotation center to thedraw
method rather than theloadSVG
method, and in_drawFromImage
, the offsetting and resizing would be applied to the canvas that the SVG is being rendered to.This works fine... in Chrome. But to implement this, you need to call
ctx.drawImage
at non-integer coordinates. And in Firefox, thedrawImage
method won't render SVGs with the proper subpixel offset, instead resampling them and making them blurry.So the only cross-browser way to offset an SVG by a subpixel amount is to set its
viewBox
, like we did in our first attempt. But how do we do this without breaking mipmaps?The Practice (Attempt 3)
This is mostly the same as attempt 1, but with one key difference: instead of changing the
viewBox
and rotation center to be round numbers, we ensure they are multiples of the smallest mipmap's size.For instance, if the smallest mipmap is 1/8th the SVG's native scale, we ensure that the
viewBox
and rotation center are multiples of 8. This ensures that, even at the smallest mipmap's size, the SVG is rendered at the proper subpixel position. This is the best cross-browser solution I've found.The Demo
I have a rough version of this working in my
subpixel-v2
branches ofscratch-render
andscratch-svg-renderer
. The code is somewhat old (preceding SVG mipmaps, the removal ofgetDrawRatio
, and the synchronousscratch-svg-renderer
API), but it works well enough to eliminate the jitter and jaggies entirely:The Remaining Problem
There's a kink that still needs to be worked out, however. Expanding the viewbox out to multiples of n pixels will increase the size of costumes' bounds. This isn't a problem for
getFastBounds
(which doesn't return consistent results and doesn't need to) andgetConvexHullPointsForDrawable
(the size of the convex hull won't be affected at all), but it presents a problem forgetAABB
, which is used (or rather, should be used) for fencing sprites.With the viewbox-expanding behavior in place, there will be a split between a costume's "logical bounds" (aka the
viewBox
), and its "texture bounds" (the dimensions of the quadrilateral rendered by WebGL).In order to calculate both the "logical bounds" (for fencing behavior) and "texture bounds" (for rendering), it looks to me like
Drawable._calculateTransform
will have to be refactored in some way-- by changing it to calculate both the "logical" and "texture" bounds, by splitting it into two different methods which calculate the two different bounds, or in some other (hopefully less complex) way.EDIT: Distortion effects will also have to change what they consider the "center" of the sprite.
If you have any ideas for avoiding complexity here, I'd greatly appreciate them.
The text was updated successfully, but these errors were encountered: