pixijs / pixijs

The HTML5 Creation Engine: Create beautiful digital content with the fastest, most flexible 2D WebGL renderer.
http://pixijs.com
MIT License
43.95k stars 4.78k forks source link

Bug: SVGs containing polygon with sub-pixel coordinates render weird artifacts #11060

Open momer opened 3 days ago

momer commented 3 days ago

Current Behavior

Attached are three SVG files. I extracted the shapes from a larger SVG asset, containing many paths, after narrowing down the issue to the triangle shape. Looking at the path itself, it looks like the issue may be related to its sub-pixel polygon points.

  1. Rock
  2. Triangle (polygon-based)
  3. Rock and Triangle (still polygon-based).

1 and 2 render the SVG shapes as expected, as the browser does. 3 results in a shape which is neither. And, when these two shapes are combined as part of a larger SVG, with other paths, the artifact stretches and takes a bit of a different shape, but along the same path (seemingly).

SVG images

  1. Rock

Image

  1. Triangle

Image

<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" width="960" height="560" viewBox="0 0 960 560">
    <g id="Layer_1-2">
        <polygon points="479.84 279.78 480.19 279.81 480 280.09 479.84 279.78" style="fill:#2b2c38;"/>
    </g>
</svg>
  1. Rock and Triangle

Image

Rendered results of Rock and Triangle (pixi.js @ ^8.5.2)

Image

Expected Behavior

The triangle should render as a triangle. That said, the triangle looks fine in Illustrator, and the only way I narrowed down the bug to this particular element was by deleting other elements on the stage until it was clear that this element was causing the issue.

Steps to Reproduce

Here's a pixi-react page with components to render the issue

import { Application, extend, useApplication, useAssets, useTick } from '@pixi/react';
import { Container, Graphics, Sprite } from 'pixi.js';
import { forwardRef, useEffect, useRef, useState } from 'react';

import rockSvgUrl from '/static/images/pixi/rock-triangle.svg';
import { ApplicationState } from '@pixi/react/types/typedefs/ApplicationState';

extend({
    Container,
    Graphics,
    Sprite,
});

export const RockContainer = () => {
    const [rockTrianglePivot, setRockTrianglePivot] = useState({ x: 0, y: 0 });
    const [rockTriangleScale, setRockTriangleScale] = useState<number>(1);
    const { app }: ApplicationState = useApplication();
    const {
        assets: [
            rockTriangle,
        ],
        isSuccess,
    } = useAssets([
        {
            alias: 'rock-triangle',
            src: rockSvgUrl,
            data: { parseAsGraphicsContext: true }
        }
    ]);

    const ref = useRef<Graphics>(null);

    useTick(() => {
        if (rockTriangle && isSuccess && ref && ref.current && ref.current?.constructor && ref.current.constructor.name === 'Graphics') {
            setRockTriangleScale(3);
        }
    });

    useEffect(() => {
        if (isSuccess && rockTriangle && app?.renderer && app?.ticker && app?.screen) {
            const bounds = rockTriangle.bounds;
            setRockTrianglePivot({
                x: (bounds.x + bounds.width) / 2,
                y: (bounds.y + bounds.height) / 2,
            });

        }
    }, [rockTriangle, app]);

    return (
        <container sortableChildren={ true }>
            { isSuccess && rockTriangle && app?.renderer && app?.screen && (
                <graphics
                    ref={ ref }
                    context={ rockTriangle }
                    pivot={ rockTrianglePivot }
                    scale={ rockTriangleScale }
                    x={ app.screen.width /2}
                    y={ app.screen.height /2 - 100}
                />
            ) }
        </container>
    );
};
RockContainer.displayName = 'HomeDemo';

export const HomeDemo = forwardRef<HTMLDivElement>((props, ref) => {
    return (
        <Application
            autoStart
            sharedTicker
            resizeTo={ ref }
            background={ 'white' }
            backgroundAlpha={ 0 }
        >
            <RockContainer/>
        </Application>
    );
});
HomeDemo.displayName = 'HomeDemo';

Environment

Possible Solution

No response

Additional Information

This issue is most painful when importing pre-existing assets, and trying to identify the source of artifacts rendering around the Graphic. The artifact occurred ~800px away from the little triangle buddy.

GoodBoyDigital commented 3 days ago

hey @momer , there is an issue with how the paths are triangulated. We use a much faster, but less accurate triangulation method for pixi shapes including svgs.

Perhaps we could add a plugin that uses something like this to triangulate it more thoroughly (albeit slower!)

momer commented 3 days ago

@GoodBoyDigital If performance of parsing the initial path is a concern, it might make sense to cache the instructions associated with the SVG, and de-couple them from the GraphicsContext. It would also be cool to have SVGParser become an interface, and allow users to select from FastSVGParse and AccurateSVGParse implementations.

On the caching side, copy-on-write seems like a good approach, as the common use-case is probably creating one or many instances of the same SVG, and perhaps changing any instructions (e.g. path fill), warrants a copy; although, even the styles could be decoupled from the instruction paths - depending on what the common usage path is.

Somewhat related: a decoupling in any case would also be great for supporting things like SVG Groups - I dropped some ideas about that use case specifically today in discord.

I'm also pretty new to Pixi and animating stuff, so may be completely off-base with those thoughts, as I'm only really familiar with my usage!

Ninja edit: I'm also happy to help work on this!

GoodBoyDigital commented 2 days ago

I like the idea of SVG groups - I think that we could suppor them within the context it as a collection of paths / instructions - unless the goal is to reuse them reuse them in other contexts or save us from building groups more than once?

I think focusing on having a new method to triangulate the shapes, would be an great thing to add to Pixi! Moves us closer to being a ble to support even more svgs! Would love a hand here - we are always looking for smart cookies to help make pixi more awesome!

momer commented 2 days ago

Supporting SVG groups would be rad - my use case depends on referencing individual group's paths for creating other Sprites/Graphics. It is a bit of a bummer that an SVG is reloaded and parsed for each Graphics object, though!