[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)