SolidCode / SolidPython

A python frontend for solid modelling that compiles to OpenSCAD
1.14k stars 178 forks source link

Ways to speed up OpenSCAD .STL rendering? #72

Closed nerdfever closed 7 years ago

nerdfever commented 7 years ago

This is probably more of an OpenSCAD issue than a SolidPython issue, but SolidPython makes it easy (maybe too easy) to generate code that takes forever to render.

For example, the SolidPython script below took more than 30 minutes to render (on an i7 box).

I'm quite sure the problem is using "for offset in frange" (near the end of the script) together with $fs=1000.

Is there some general technique I ought to be using to reduce the render time? Somehow maybe merge objects, etc?

#! /usr/bin/env python
# -*- coding: UTF-8 -*-
# (c) 2017 nerdfever.com

# All units are mm and degrees unless mentioned otherwise

import math

# SolidPython
from solid import *
from solid.utils import *

# tweakables
clearance = 0.15
thickness = 3.0
tubeDiameter = 54

# ref https://en.wikipedia.org/wiki/Unified_Thread_Standard
screw_2_56_diameter = 2.1844
screw_4_40_diameter = 2.8448
screw_6_32_diameter = 3.5052

# flange focal distances
pentax_q_ffd    = 9.2
leica_m_ffd     = 27.8
b4_ffd          = 48

tubeLength = (b4_ffd - leica_m_ffd) - 2*thickness - 0.8 # -0.0 was too long (DL3), -1.0 was too short (DL4)

def scad_render(model, circle_segments=1000):
    scad_render_to_file(model, file_header='$fn = %s;' % circle_segments, include_orig_code=True)

def ring(do, di, h):
    """
    Returns a ring (cylinder with a hole in it).
    do = outer diameter
    di = inner diameter
    h = height
    """
    return cylinder(r=do/2, h=h) - down(h)(cylinder(r=di/2, h=3*h))#.set_modifier("#")

def b4_camera_side(tall=10):
    """ 
    B4 lens mount - camera side 
    B4 flange surface is at Z origin
    Bottom of mount is at z = -thickness
    """
    # mount body
    bodyDiameter = 51.6 + 2*clearance

    # flange petals (x3)
    petalSubtendsDegrees = 48
    petalDiameter = 55.3 + 2*clearance
    petalWidth = 20 + 2*clearance
    petalThickness = 2.02 + clearance + 0.25 # +0.0 didn't fit

    # mount collar
    collarDiameter = 42 + 2*clearance

    # alignment screw
    screwDiameter = 3.5 + 2*clearance + 2.0 # +0.0 (DL3), +0.5 (DL4) didn't fit
    screwHeight = 2.3 + clearance
    screwOffAxis = 23.25

    # thumb lock screw
    thumbscrewDepth = 9.2
    thumbscrewSheathDiameter = thickness + petalThickness + thickness

    ###############################################################################################################################

    #petalWidth = math.cos(math.radians(90-petalSubtendsDegrees/2)) * bodyDiameter # 21.109631775634032

    petal = translate([0, petalDiameter/2, 0])(cube([petalWidth, petalDiameter/2, tall], center=True))#.set_modifier("#")
    petals = petal + rotate([0,0,360/3])(petal) + rotate([0,0,-360/3])(petal)#.set_modifier("#")

    def outer(thick):
        return ring(do=thickness + petalDiameter + thickness, di=petalDiameter, h=thick)

    def inner(thick):
        return ring(do=petalDiameter, di=bodyDiameter, h=thick)

    screwChannel = ring(do=screwOffAxis*2 + screwDiameter/2, di=screwOffAxis*2 - screwDiameter/2, h=screwHeight + 1)
    screwChannel *= rotate([0, 0, 2.15*petalSubtendsDegrees])(petal) + rotate([0, 0, 1.7*petalSubtendsDegrees])(petal)

    flange = outer(thickness) + ring(do=petalDiameter, di=collarDiameter, h=thickness)

    # flange surface is at Z origin
    flange = down(thickness)(flange) - down(screwHeight)(screwChannel)

    lockscrewSheathLength = thumbscrewDepth - (petalDiameter/2 + thickness - bodyDiameter/2) + thickness/2
    lockscrewSheath = translate([-(petalDiameter/2 + thickness/2), 0, petalThickness/2])(rotate([90,0,-90])(cylinder(r=thumbscrewSheathDiameter/2, h=lockscrewSheathLength)))

    lockscrewHole = translate([-(bodyDiameter/2), 0, petalThickness/2])(rotate([90,0,-90])(cylinder(r=screw_6_32_diameter/2, h=thumbscrewDepth+1)))

    slot = outer(petalThickness) 
    slot += inner(petalThickness) - petals - rotate([0, 0, -petalSubtendsDegrees*0.5])(petals) - rotate([0, 0, -petalSubtendsDegrees*1.05])(petals)#.set_modifier("#") 

    body = outer(thickness) + (inner(thickness) - petals)

    body = up(petalThickness)(body)#.set_modifier("#")

    return (flange + slot + body  + lockscrewSheath) - lockscrewHole 

def leica_m_lens_side(tall=10):
    """ 
    Leica M lens mount - lens side 
    Leica M mount flange surface is at Z origin
    Top of mount is at z = thickness
    All dimensions from my own measurment plus staring at US patent 2643581A (1951)
    """
    # mount body
    bodyDiameter = 41 - clearance
    gapWidth = 5 + clearance
    flangeDiameter = 54

    # flange petals (x4) (numbers are clockwise 1..4 from index pin, looking at camera)
    petal1Width = 11
    petal1Angle = (30 + 58)/2   - 0.2
    petal2Width = 11
    petal2Angle = (120 + 150)/2 - 0.4
    petal3Width = 14
    petal3Angle = (207 + 245)/2 + 0.8
    petal4Width = 14
    petal4Angle = (286 + 325)/2 + 0.8
    petalDiameter = 44 - clearance
    petalThickness = 1.65

    petalRampDepth = 1.5
    petalRampStep = 0.1

    # alignment index pin
    indexDiameter = 3 + 2*clearance
    indexHeight = 1.5 + clearance
    indexOffAxis = bodyDiameter/2 + indexDiameter/2 + 2.6

    ###############################################################################################################################

    petal1 = rotate([0, 0, -petal1Angle])(translate([-petal1Width/2, 0, 0])(cube([petal1Width, petalDiameter, tall])))#.set_modifier("#") 
    petal2 = rotate([0, 0, -petal2Angle])(translate([-petal2Width/2, 0, 0])(cube([petal2Width, petalDiameter, tall])))
    petal3 = rotate([0, 0, -petal3Angle])(translate([-petal3Width/2, 0, 0])(cube([petal3Width, petalDiameter, tall])))
    petal4 = rotate([0, 0, -petal4Angle])(translate([-petal4Width/2, 0, 0])(cube([petal4Width, petalDiameter, tall])))

    petals = petal1 + petal2 + petal3 + petal4

    outer = ring(do=petalDiameter, di=bodyDiameter, h=petalThickness)
    inner = ring(do=bodyDiameter, di=bodyDiameter - 2*thickness, h=petalThickness)

    leaves = outer * petals + inner

    zPosition = petalThickness

    gap = up(zPosition)(ring(do=bodyDiameter, di=bodyDiameter-thickness, h=gapWidth))#.set_modifier("#") 

    zPosition += gapWidth

    indexHole = up(zPosition-1)(cylinder(r=indexDiameter/2, h=indexHeight+1))
    indexHole = translate([math.sin(math.radians(petal4Angle)) * indexOffAxis, math.cos(math.radians(petal4Angle)) * indexOffAxis, 0])(indexHole)

    flange = up(zPosition)(ring(do=flangeDiameter, di=bodyDiameter-thickness, h=thickness))#.set_modifier("#") 
    flange -= indexHole

    mount = down(zPosition)(leaves  + gap + flange)

    for offset in frange(petalRampDepth-petalRampStep, 0, -petalRampStep):
        leaf = inner + petals * ring(do=bodyDiameter + 2*offset, di=bodyDiameter, h=petalRampStep)
        mount += down(zPosition + petalRampDepth-offset)(leaf)

    return mount

def adapter():

    # tube runs from underside of B4 flange (z=0 less thickness) to Leica M flange
    innerTube = down(tubeLength+thickness)(ring(do=42 + 2*clearance + thickness, di=42 + 2*clearance, h=tubeLength))#.set_modifier("#")
    outerTube = down(tubeLength+thickness)(ring(do=tubeDiameter, di=tubeDiameter - 2*thickness, h=tubeLength))#.set_modifier("#")

    return b4_camera_side() + outerTube + innerTube + down(tubeLength + 2*thickness)(leica_m_lens_side())

if __name__ == '__main__':
    scad_render(adapter())
etjones commented 7 years ago

I don't have any bulletproof way to speed things up, but I agree with your diagnosis: very fine iteration and very smooth circles. The CGAL library OpenSCAD uses turns everything into polygons and gets slow really fast when you add more.

One thing I did check: when I first started using OpenSCAD (2011, so no guarantees for current versions). I could find no performance difference between native OpenSCAD loops and unrolled loops like SolidPython produces; a two-liner that evaluated into 10000 shapes took exactly as long to render as literally writing out/parsing each of those shapes in 10000 lines.

I assume you're going to 3D print this piece? I'd print a 5mm tall ring of the appropriate diameter with 'circle_segments' at 50, 100, 200, 500, and 1000. See if you really need all those segments at your intended size or if you could afford to go lower. The other trick I use is to have a CIRCLE_SEGMENTS constant I keep at something low during development and only crank up when I'm doing a final render.

There may be some extra tricks to get better performances out of CGAL, but I don't know them offhand. As far as I understand, it boils down to more polygons == more render time. Good luck!

BarnacleDuck commented 7 years ago

see https://github.com/SolidCode/SolidPython/pull/65 If you have a lot of unions, it may speed up the render.

evandrone commented 7 years ago

TL;DR if unioning a lot of things, shape += [a, b, c] will currently be a lot faster than shape += a; shape+= b; shape += c

Good point! I'd forgotten that detail. In the current code, if you do something like:

shape += a
shape += b
shape += c

that translates to

union()(
    union()(
        union()(
            shape
            a,
        ),
        b
    ),
    c
)

as opposed to the much more optimal:

union()(
    shape,
    a,
    b,
    c
)

As @BarnacleDuck points out, there's a PR, #65, that would improve this behavior, but I haven't integrated it yet.

aarchiba commented 7 years ago

You may have better luck using $fa to set angular limits on the number of segments, and $fs to set minimum segment sizes - for my 3D printer there's no point in a segment less than 50 microns or an angle less than a degree. These settings generate large but not completely outrageous models.

Fundamentally, though, CGAL is incredibly demanding when handed complex polygon models. I had one heightmap, maybe a megapixel, that required more than my laptop's 16 GB of RAM to render. (Also OpenSCAD's heightmaps seem to make more polygons than necessary.) It's unclear to me whether any CSG library is capable of coping with such models, though I suspect if CGAL was smarter with spatial data structures to accelerate polygon intersection it could become much less demanding without losing desirable numerical properties. For smooth objects the OpenCascade kernel can work with NURBS, keeping objeect numbers small, but that won't help with heightmaps or other inherently-polygonal objects.