[HN Gopher] Show HN: I made a 3D SVG Renderer that projects text...
___________________________________________________________________
Show HN: I made a 3D SVG Renderer that projects textures without
rasterization
Author : seveibar
Score : 195 points
Date : 2025-06-05 02:05 UTC (20 hours ago)
(HTM) web link (seve.blog)
(TXT) w3m dump (seve.blog)
| JKCalhoun wrote:
| Subdivision is a good trick.
|
| A friend was writing a flight simulator from scratch (using
| _Foley and van Dam_ as reference for all the math involved). A
| classic perspective problem might be a runway.
|
| Imagine a regularly spaced dashed line down the runway. If you
| get your 3D renderer to the stage that you can texture quads with
| a bitmap, it might seem like a simple thing to have a large
| rectangle for the runway, a bitmap with a dashed line down the
| center for the texture.
|
| But the texture mapping will not be perspective (well, not
| without a lot of complicated math involved).
|
| _Foley and van Dam_ say -- break the runway into a dozen or so
| "short" runways laid end to end (subdivide). The bitmap texture
| for each is just a single short stripe. Now because you have a
| bunch of these quads end to end, it is as if there is a longer
| runway and a series of dashed lines. And while each individual
| piece of the runway (with a single stripe), is not in itself
| truly perspective, each quad as it gets farther from you is
| nonetheless accounting for perspective -- is smaller, more
| foreshortened.
| kibibu wrote:
| Perspective correct texture mapping has been solved for quite
| some time without excessive subdivision.
|
| It was avoided in the Foley and Van Dam days because it
| requires a division per rasterized pixel, which was very slow
| in the late 80s.
| taylorius wrote:
| Back in the early 90s I did a version of Bresenham's
| algorithm that would rasterize the hyperbolic curves that
| perspective-correct texture mapping required. It worked
| correctly though the technique of just doing a division every
| n pixels and linearly interpolating won out in the end, if I
| recall.
| rixed wrote:
| You could also avoid divisions entirely, while still keeping
| 100% correct perspective, by "rasterizing" the polygon
| following the line of constant Z. You would save the divs,
| but then you would draw mostly outside the cache, so not a
| panacea, but for large surfaces it was noticeably nicer than
| divide-every-N-pixcels approximation.
| JKCalhoun wrote:
| Some wild stuff about "defs" that I was unaware of in SVGs.
| seveibar wrote:
| Defs saved the day here on file size- repeating the image
| (which we usually base64 encode) would have caused a much
| larger file size and made rasterization much more appealing!
| 90s_dev wrote:
| Defs is also how the arrows work in this WebGL2 diagram[2], and
| in fact, I don't think they're possible _without_ defs, because
| of `marker-end` which seems to require a marker present in
| defs.
|
| [2]
| https://webgl2fundamentals.org/webgl/lessons/resources/webgl...
| ndgold wrote:
| What's the sota for 2d object diagrams to 3d cad output?
| dedicate wrote:
| This is seriously cool! I've always mentally boxed SVG into the
| 2D corner, so seeing it handle 3D projection like this is pretty
| mind-bending...
| rollulus wrote:
| I'm afraid your CSS triangles are still rendered through
| rasterization but a good job nonetheless.
| bufferoverflow wrote:
| But he isn't limited to one specific resolution. If he used
| PNG, he would be limited.
| jesse__ wrote:
| "it's a lightweight SVG renderer"
|
| Meanwhile.. drawing 512 subdivisions for a single textured quad.
|
| It's a cute trick, certainly, but ask this thing to draw anything
| more than a couple thousand elements and I bet it's going to roll
| over very quickly.
|
| Just use webgl where perspective-correct texture mapping is built
| into the hardware.
| seveibar wrote:
| The goal for this vanilla TS renderer is to have visual diffing
| on GitHub and a renderer that works without a browser
| environment. Most 3D renderers focus on realtime speed, not
| file size and runtime portability. I think in practice we will
| configure the subdivisions at something like 64 for a good file
| size tradeoff
| kookamamie wrote:
| Why use SVG for this, though? This could be easily
| implemented as pure JS software rasterizer without all the
| tessellation workarounds.
| ricardobeat wrote:
| > The goal for this vanilla TS renderer is to have visual
| diffing on GitHub and a renderer that works without a
| browser environment
| itishappy wrote:
| This doesn't answer the question. If you're doing all
| this work in JS to render a static SVG, why not just "do
| it right" and output a static PNG instead?
| seveibar wrote:
| The top of the PCB (the lines etc) are computed as an
| SVG, i would have to have an SVG rasterizer just to begin
| with that approach, then would be limited by what images
| I could rasterize. It would also be much much slower than
| quickly computing matrices
| jahewson wrote:
| You might find that librsvg works for you.
| itishappy wrote:
| I was going to suggest raylib for server-side rendering,
| but it adds a non-JS dependency. Apparently it has
| optional support for rendering SVGs to textures.
|
| https://github.com/raysan5/raylib/discussions/3741
| gyf304 wrote:
| It's worth noting that this same restriction of not being able to
| do perspective transformations is also one of the defining
| characteristics of PlayStation 1 graphics. And the workaround of
| subdivision is also the same workaround PS1 games used.
|
| More reading:
| https://retrocomputing.stackexchange.com/questions/5019/why-...
| bhouston wrote:
| It is also a limitation that many initial DOS 3D software
| rasterized games had (e.g. Descent.)
|
| This is because perspective transform requires a divide per
| pixel and it was too costly on the CPUs of the time, so they
| skipped it to get acceptable performance.
| BearOso wrote:
| It's also commonly known that Quake only did a perspective
| divide every 16 pixels.
|
| It's funny that, in today's CPUs, floating point divide is so
| much faster than integer divide.
| bn-l wrote:
| Huh that's so crazy. I had that in my head as I was reading the
| article. I was thinking about some car game and the way the
| panels would look when it rotated in your "garage".
| est wrote:
| I remember someone made a 3D renderer in IE5.5 using csss border
| triagles. Voronoi diagrams and stuff.
| unwind wrote:
| Very nice-looking for being SVG!
|
| One possibly uncalled-for piece of feedback: is that USB-C
| connection finished, and is it complying with the various
| detection resistor requirements for the CCx pins? It seemed very
| bare and empty, I was expecting some Rd network to make the
| upstream host able to identify the device. Sorry if I'm missing
| the obvious, I'm not an electronics engineer.
|
| See [1] for instance.
|
| [1]: https://medium.com/@leung.benson/how-to-design-a-proper-
| usb-...
| seveibar wrote:
| Because it's only being used for power and doesn't need a lot
| of power, it works for the simple board we rendered. In
| practice you would absolutely want to set the CC1 and CC2
| configuration with resistors!
| laszlokorte wrote:
| Very cool! Just just implemented an SVG 3D renderer a few weeks
| ago [1]. But I did not implement texturing yet and wondered how
| one could do this.
|
| [1]: https://youtu.be/kCNHQkG1Q24?si=3VxfVFtG2MiEEmlX
| badmintonbaseba wrote:
| An other approach would be to apply the transformation to SVG
| elements separately. Inkscape has a perspective transformation
| tool, which you can apply to paths (and paths only). It
| probably needs to do approximation and subdivision on the path
| itself though, which is possibly more complex.
| seveibar wrote:
| Your renderer looks awesome! I was surprised there wasn't an
| "off the shelf" SVG renderer in native TS/JS, it's a big deal
| to be able to create 3D models without a heavy engine for
| visual snapshot testing!
| CrimsonCape wrote:
| When you loaded Suzanne, my eye could detect framerate drop
| when moving the model. What is the hot path in the
| calculations?
| weinzierl wrote:
| This is a cool project and I think I can use that. I was just
| wondering if perspective correctness was all that important for a
| PCB renderer? The distortion should be minimal for these kind of
| images and I think old CAD programs often did not use correct
| perspective as well.
| seveibar wrote:
| We could absolutely use isometric projection, but personally I
| find them a bit hard to visually parse.
| stuaxo wrote:
| Awesome. If this gets really popular I could imagine perspective
| transforms being proposed for SVG itself.
| moron4hire wrote:
| Three.js has had an SVG rendering back end for 13 years. It's
| going to be pretty hard to get much more popular than Three.js
| to get over the browser vendors' reluctance to make any changes
| to SVG.
| chrismorgan wrote:
| I'm not certain, but I think Firefox just implemented 3D
| transformations for SVG from the start. It wasn't exactly hard
| to conceive. Certainly by mid-2017 it had it. Somewhere around
| that time there was also concerted effort toward aligning SVG
| and CSS.
|
| (Firefox's implementation does still suffer from one long-
| standing bug which means you want to make sure your viewbox
| unit is larger than one device pixel, but that's normally not
| hard to achieve.
| https://oreillymedia.github.io/Using_SVG/extras/ch11-3d.html...
| shows what it's about. I don't really understand why that
| problem isn't fixed yet; what I _presume_ is the underlying
| issue affects some HTML constructs too when you scale things
| up, and surely it's not _that_ rare? I know I found one such
| problem a decade ago (and, being in HTML, it couldn't be worked
| around like you can with SVG). They've improved things a bit,
| but not entirely.)
|
| Sadly, no one else seemed all that interested in making 3D
| transformations work properly in SVG content.
| badmintonbaseba wrote:
| I don't think your algorithm is correct. At least on the
| checkerboard example on the cube face the diagonals are curved.
| Perspective transformation doesn't do that.
|
| Possibly you do the subdivisions along the edges uniformly in the
| target space, and map them to uniform subdivisions in the source
| space, but that's not correct.
|
| edit:
|
| Comparison of the article's and the correct perspective
| transform:
|
| https://imgur.com/RbRuGxD
| Karliss wrote:
| Considering that the author considers math below his pay-grade
| not a huge surprise that it is wrong.
| frizlab wrote:
| YES! I was taken aback by that statement too. I think the
| opposite: in this age of AI, actually _knowing_ things will
| be a huge bonus IMHO.
| jeremyscanvic wrote:
| Also known as the good ol' straight lines remain straight in
| perspective drawing!
| ricardobeat wrote:
| Is it actually possible to draw the correct perspective using
| only affine transformations? I thought that was the point of
| the article.
| badmintonbaseba wrote:
| It is possible to approximate perspective using piecewise
| affine transformations. It is certainly possible to match the
| perspective transformation at the vertices of the
| subdivisions, and only be somewhat off within.
| itishappy wrote:
| With 6 degrees of freedom, you can only fit 3 2d points at
| a time. Triangulation causes the errors shown in the
| article, hence why subdivision is needed.
| jeremyscanvic wrote:
| I think GP's point is that besides the unavoidable
| distortions coming from approximating a perspective transform
| by a piece-wise affine transform, the implementation remains
| incorrect.
| mistercow wrote:
| Even more obviously, the squares in the front aren't bigger
| than the squares in the back. It looks like each square has
| equal area even as their shapes change.
|
| It's fascinating how plausible it looks at a glance while being
| so glaringly wrong once you look at it more closely.
| seveibar wrote:
| I've updated the article with the fixed projection transform!
| I had to make an animation as well just to validate it- I
| fooled myself!
| jeremyscanvic wrote:
| The fixed rendering looks really nice. Good job!
| seveibar wrote:
| Author here: I don't think the commenter here has set the same
| focal length, the focal length can make a surface appear
| curved, I set it explicitly to a low value to test the
| algorithm's ability to handle the increased distortion. You can
| google "focal length distortion cube" to see examples of how a
| focal length distorts a grid or you can google "fish eye lens
| cube" etc.
|
| Edit: I think there's a lot of confusion because the edges of
| the cube (the black lines), do not incorporate the perspective
| transform all along their edge. The texture is likely correct
| given the focal length, and the cube's edge is misleadingly
| straight. My bad, the technique is valid, but the black lines
| of the cube's edge are misleadingly straight (they are not
| rendered the same way as the texture)
| Masterjun wrote:
| I think the original commenter is correct that there is a
| mistake in the perspective code. It seems the code calculates
| the linear interpolation for the grid points too late. It
| should be before projecting, not after.
|
| I opened an issue ticket on the repository with a simple
| suggested fix and a comparison image.
|
| https://github.com/tscircuit/simple-3d-svg/issues/14
| seveibar wrote:
| That admittedly looks a lot more correct! Thanks for
| digging in, i will absolutely test and submit a correction
| to the article (i am still concerned the straight edges are
| misleading here)! And thanks to the original commentor as
| well! I think I will try to quickly output an animated
| version of each subdivision level, the animation would make
| it a lot more clear for me!
| jeremyscanvic wrote:
| I might be missing something but you sound genuinely confused
| to me. The perspective in your post is linear perspective.
| It's the one used in CSS and it doesn't curve straight
| lines/planes. It's not the perspective of fish-eye images
| (curvilinear perspective).
| seveibar wrote:
| I was at least a little confused because yea fish eye isn't
| possible with a 4x4 perspective transform matrix. I'm
| investigating an issue with the projection thanks to some
| help from commenters and there will be a correction in the
| article, as well as an animation which should help confirm
| the projection code.
| m-a-t-t-i wrote:
| Interesting, I've been doing 3D SVG by storing the xyz-
| coordinates in a separate array and using inlined javascript to
| calculate & refresh the 2D coordinates of the SVG items
| themselves after rotation. But this means that the file only
| works in a browser. Maybe it could be possible to replace the
| javascript with native functions, so the same file would work
| everywhere.
| badmintonbaseba wrote:
| How do you transform paths? Do you just transform the control
| points?
| m-a-t-t-i wrote:
| Yeah, paths are saved in an array where each path segment is
| a list of control points coupled with the corresponding path
| command (M, L, C). Those can be used to recreate the path
| item.
| itishappy wrote:
| Since the final SVG will have a set perspective and still
| requires rendering... What's the benefit over rendering an image?
| seveibar wrote:
| Very small files and a much simpler rendering scheme! I don't
| have to rasterize my SVGs that represent the top of my board
| itishappy wrote:
| > Very small files and a much simpler rendering scheme!
|
| For a 400x400 SVG with 6 surfaces and 64 subdivisions your
| file size is only 10x smaller than an uncompressed bitmap.
| Your SVG should scale linearly with number of objects and be
| constant with resolution, while an image would scale with the
| resolution (quite favorably if compressed) and be constant
| with the number of objects. I'd be interested to know the
| size of the example at the top of the article.
|
| Also you already have the math to transform points!
|
| > I don't have to rasterize my SVGs the represent the top of
| my board.
|
| Ahhhhhh. This clears it all up!
| iamleppert wrote:
| What does he think SVG is doing under the hood? Rasterization.
| Everything does rasterization at some point in the process.
| Calculating 512 clip paths to render a single quad that could be
| drawn in a single for loop is insane.
| itishappy wrote:
| SVG has no concept of 3d space so you'd have to write your own
| SVG rasterizer if you want it to render perspective.
| rixed wrote:
| ...and transfer all those pixels to the browser.
| leptons wrote:
| SVG is the wrong tool for this job.
| looneysquash wrote:
| What about using the filters? Could you do something with
| https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/E... ?
| rixed wrote:
| This is nice, but the article left me unconvinced that you need
| textures at all. Be it a checker or the drawing on a circuit
| board, can't you keep everything as vectors, thus avoiding the
| problem entirely?
| seveibar wrote:
| Circuit boards have holes, cutouts and import STL/OBJ
| components that we'll eventually support in this 3d renderer.
| Assuming we get that far I may have to rename it from
| "simple-3d-svg"!
| leptons wrote:
| I think you'll probably run into performance problems with
| SVG before you get too far. I can't imagine SVG will perform
| fluidly with complex circuit boards.
|
| SVG elements are DOM elements after all, and too many DOM
| elements will cause browser performance issues. I know this
| the hard way, after adding a few hundred SVG <path> elements
| with a few hundred <div> elements in a React-based
| interactive web application, I ended up needing to move to a
| canvas solution instead, which works amazingly well.
|
| I really hope you have all that figured out, because I don't
| think it's going to work well using SVG to render complex
| circuit boards. But maybe your product is only working with
| very simple circuit boards?
| exabrial wrote:
| I hope someday where we get back to a simple HTML/CSS standard
| for "text" pages and that's it. No JavaScript, no DOM. This
| covers 70% of the web use cases.
|
| "Everything else" would be a pluggable execution runtime that are
| distributed as browser plugins: [WASM Engine, JVM engine, SPIR-V
| Engine, BEAM Engine, etc] with SVG as the only display tech. The
| last thing we'd define is an interrupt and event model for system
| and user interactions.
| leptons wrote:
| When things like Three.js exist, developing an SVG 3D engine to
| display circuit boards seems like a ridiculous thing to do.
|
| Why did you feel you had to do this with SVG?
___________________________________________________________________
(page generated 2025-06-05 23:01 UTC)