How to export 3D geometry to SVG

I’m coming to OPENRNDR from Processing, and I’ve found something comparable for everything I used to do there except for rendering 3D geometry to SVG. Processing’s SVG package had a way to do this easily. According to their documentation:

To create vectors from 3D data, use the beginRaw() and endRaw() commands. These commands will grab the shape data just before it is rendered to the screen. At this stage, your entire scene is nothing but a long list of lines and triangles. This means that a shape created with sphere() method will be made up of hundreds of triangles, rather than a single object.

When using beginRaw() and endRaw(), it’s possible to write to either a 2D or 3D renderer. For instance, beginRaw() with the SVG library will write the geometry as flattened triangles and lines.

It doesn’t seem like this is currently implemented by OPENRNDR, but if I wanted to get this “shape data just before it’s rendered to the screen”, where would I look?

My fallback approach to the above is to do this flattening in a more manual way using the drawer’s transformation matrices, but that seems like it’s going to be a lot less optimized and less flexible.

Following up on my own post: for my “fallback” approach, the project function gets me much of the way there in terms of mapping 3D geometry to a 2D viewport

Hi! Welcome to the forum and good find about Transforms.project()!

I’ll ask if there is a way to apply a perspective camera to the drawComposition.

A different approach I’ve used is to generate 3D shapes, then open them in Blender and use it’s SVG exporter, which does occlusion and can render edges only if I remember right…

2023-04-25_09-35

One thing I was wondering is… could the matrix here be somehow multiplied by the cam matrix to get perspective?

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.perspective
import org.openrndr.math.transforms.transform
import org.openrndr.shape.Rectangle
import org.openrndr.shape.drawComposition

fun main() = application {
    program {
        val cam = perspective(
            60.0, width * 1.0 / height,
            0.001, 100.0
        )
        val rect = Rectangle.fromCenter(Vector2.ZERO, 300.0, 200.0).contour
        val comp = drawComposition {
            stroke = ColorRGBa.BLACK
            repeat(12) {
                val mat = transform {
                    translate(width * 0.5, height * 0.5)
                    rotate(Vector3.UNIT_Z, 10.0 * it)
                    rotate(Vector3.UNIT_Y, 10.0 * it)
                    rotate(Vector3.UNIT_X, 15.0 * it)
                } // * cam
                contour(rect.transform(mat))
            }
        }
        // comp.saveToFile(File("output.svg"))
        extend {
            drawer.clear(ColorRGBa.WHITE)
            drawer.composition(comp)
        }
    }
}

Yeah I’ve actually gone down the Blender + Freestyle SVG exporter route before. It’s incredibly useful, but I’m not as good at using Blender as I am at just coding.

As for the “perspective” part, I think you can get much of the way there using the project function again, unless I’m misunderstanding. I’m using the Orbital camera, which I think populates the transform values in drawer, so ultimately I use the the drawer’s model/view/projection matrices as arguments for project.

This is a snippet of my SVGExporter code:

fun exportToSVG(lineShapes: List<LineShape>, drawer: Drawer): Composition {
    val root = GroupNode()
    val composition = Composition(root)

    for(lineShape in lineShapes){
        var g = GroupNode()
        for(line in lineShape.getLines()){
            val p1 = project(line.start, drawer.projection, drawer.view, drawer.width, drawer.height)
            val p2 = project(line.end, drawer.projection, drawer.view, drawer.width, drawer.height)
            val lineSegment = LineSegment(p1.xy, p2.xy)
            val shapeNode = ShapeNode(lineSegment.shape)
            shapeNode.stroke = ColorRGBa.BLUE
            g.children.add(shapeNode)
        }
        root.children.add(g)
    }

    return composition
}

The line objects basically just have a start and end, both are Vector3. I’m sure there’s a more elegant way of doing something similar inside the drawComposition code longer term.