mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
102.01k stars 35.33k forks source link

SVGLoader: Shape generation breaks with large stroke width. #25326

Open Mugen87 opened 1 year ago

Mugen87 commented 1 year ago

Description

The original issue was reported at the forum: https://discourse.threejs.org/t/glitches-with-svg-loader/46803

SVGs that exceed a certain stroke-width are not rendered correctly. As visible in the below screenshot, the resulting shape is rendered with additional triangles on its back side. When decreasing the stroke width (in the below SVG try 200), the issue goes away.

I suspect there is a problem in SVGLoader.pointsToStrokeWithBuffers() which generates triangles based on the stroke definition from the SVG.

Reproduction steps

  1. Load the following SVG with the loader to reproduce the glitch.
    <?xml version="1.0"?>
    <svg viewBox="0 0 600 792" width="600" height="792">
    <path stroke="#64C36E" fill="none" id="link_2_21_ok" d="M30,463.61904761904765C150,463.61904761904765,150,571.0476190476189,270,571.0476190476189" stroke-width="290" stroke-opacity="0.6"/>
    </svg>

Code

Live example

Screenshots

image

Version

r148

Device

Desktop, Mobile, Headset

Browser

Chrome, Firefox, Safari, Edge

OS

Windows, MacOS, Linux, Android, iOS

yomboprime commented 1 year ago

I had the hope that setting a high value for the number of interpolation points would correct the issue (note the extra 5000 parameter here): const geometry = SVGLoader.pointsToStroke( subPath.getPoints( 5000 ), path.userData.style );

Unfortunately it does not solve it. I'm afraid it is not addressable in the algorithm as it is written right now. It would require self-intersection tests. On the other hand it is doable in a completely separated 2D algorithm, let me see if I can put up something in the next days.

yomboprime commented 1 year ago

I tried to make a triangularization algorithm which discards overlapping triangles. But it gives false positives and discards too many. It works in tests but not in the SVG from the example.

Mugen87 commented 1 year ago

Something that came to my mind: Could you utilize the existing Earcut implementation for this use case?

https://threejs.org/docs/index.html#api/en/extras/Earcut

Meaning you first generate the contour (which is just a sequence of points) an then let Earcut do its job.

yomboprime commented 1 year ago

Thanks for noting it. I will take a look. I will also investigate if I can get the holes indices.

yomboprime commented 1 year ago

@Mugen87 I've implemented your idea, by holding two contour points arrays (left and right ones) and combining them at the end, giving the full stroke contour.

It works mostly correctly in the use case. I hoped it worked in the general case but you can see that the Tiger.svg has some artifacts. It is caused by the contour I generate, which folds over itself on some corners. Earcut does not like that on complex paths. As before, solving this would require checking self-intersections, but now in the linear contour, not in a triangle soup. It is doable, but only would work if the original path doesn't self-intersect.

Detail of the contour folding over itself:

imagen

Tiger.svg:

imagen

Use case:

imagen

Use case detail:

imagen

The algorithm is smaller and cleaner (751 lines down to 495). Performance seems similar as before. The drawback is that the two contour arrays must be allocated. This can be optimized by providing reusable user arrays.

As the whole pointsToStrokeWithBuffers() is modified, I don't know very well how to integrate this change. For now I've just duplicated the function with the name pointsToStrokeContourWithBuffers().

The function pointsToStroke() accepts a boolean parameter useEarcut (default false) and selects which function to call.

It seems THREE doesn't export Earcut so I've copied Earcut.js in /jsm just for testing.

I've tried to make a live preview but failed. You can read the modified loader and example here: https://github.com/yomboprime/three.js/commit/563aea9172ada3e7dfa862e3125334415b40f336

Expecting your feedback to see if this can be useful.

yomboprime commented 1 year ago

Just an update: There was some bugs in that branch commit. I'm trying to debug some last issues.

yomboprime commented 1 year ago

Unfortunately I've not been able to solve it for the general case. I've spent many hours on this.

The problem is when the path points do auto-intersect like this (the contour gets flipped over itself): contour_autointersection

I think this problem needs more thought than an amateur mathematician (me) can give.

Mugen87 commented 1 year ago

I appreciate your effort on this! Even without a concrete solution the problem is now better understood than before 👍 .

yomboprime commented 1 year ago

Thanks!