Rhino Surface to Bitmap Gradient

Create and paint bitmap from rhino input  in python. An example with gradients.

In rhino you need to draw:

  • Layer “Frame” > contains a rectangle with the exact size of the bitmap you want to make (1 unit = 1 pixel). Start with lower left corner at 0,0 because I was lazy to add more scripting to handle this.
  • Layer “ZONE_III” > contains the planar surfaces that contain the “end color” of the gradient – each surface as a unique name, matching a name assigned to the corresponding gradient zone.
  • Layer “ZONE_II” > contains the gradients zones (XY-planar untrimmed nurbs surfaces).

If a gradient zone is a “donut” make it so:

Loft surface between the 2 borders, starting from the interior one

If it is a “U” make it so:

Edge surface between the 2 edge curves, starting from the interior one

  • Layer “curve_g” > contains a curve that will guide the transition of the color withing the gradient (typically if it is a straight line going up diagonally, the gradient will be linear).
  • Obviously don’t put anything else on those layers.

In this example I use the script to make photoshop masks for a panorama made of 6 faces of a cube. The bitmap is the unwrapped cube. In this case, when drawing the contours of the gradients in rhino I had to be cautious that the edges match when folding back the box.

What the script does.

  • Find which pixels are in which surfaces from ZONE_III and ZONE_II.
  • Paint pixels from ZONE_III with color 1.
  • For the pixels in ZONE_II, evaluate their location on the surface along U, get a parameter between 0 and 1 (color 2 and color 1), modify this parameter if the gradient is not linear, and paint the pixels from ZONE_II in a computed color between color 2 and 1 related to the modified parameter.
  • Rest of the bitmap is created and left transparent.
 
#-------------------------------------------------------------------------------
# Name:        bitmap_gradient_from_loft
# Purpose:     make gradients from loft surfaces to use in Photoshop as masks for instance
# Author:      Marie Prunault
# Created:     26-07-2016
# Copyright:   (c) Marie Prunault 2016
#-------------------------------------------------------------------------------
import Rhino as rh
import rhinoscriptsyntax as rs
import scriptcontext as sc
import math
import System.Drawing.Color as col
import System.Drawing.Bitmap as btmp
import os
import System.Drawing.Imaging.ImageFormat as imgg
#---------------------------CONSTANTS
tol=sc.doc.ModelAbsoluteTolerance
#------------------------------------------------GENERIC STUFF BUT ACTUALLY I DON'T NEED ALL IN THE END
#I normally get those functions from my self-made library, always at hand
def dump(obj):
    '''print all members for instance of a class'''
    for attr in dir(obj):
        print "obj.%s = %s" % (attr, getattr(obj, attr))
def rhob_to_rcob(rhinoob):
    '''from GUID to rhino Geometry base, almost ready'''
    if isinstance(rhinoob,list):
        geoms=[]
        for rhinoobject in rhinoob:
            objref= rh.DocObjects.ObjRef(rhinoobject)
            geomel = objref.Geometry()
            geoms.append(geomel)
        return geoms
    else:
        objref= rh.DocObjects.ObjRef(rhinoob)
        geom = objref.Geometry()
        return geom
def BoundingBox( rhobjects, plane=rh.Geometry.Plane.WorldXY, in_world_coords=True ):
    xform=rh.Geometry.Transform.ChangeBasis(rh.Geometry.Plane.WorldXY,plane)
    bbox=rh.Geometry.BoundingBox.Empty
    for object in rhobjects:
        objectbbox=object.GetBoundingBox(xform)
        if objectbbox: bbox = rh.Geometry.BoundingBox.Union(bbox,objectbbox)
    corners = bbox.GetCorners()
    if in_world_coords and plane is not None:
        plane_to_world = rh.Geometry.Transform.ChangeBasis(plane, rh.Geometry.Plane.WorldXY)
        count=0
        for pt in corners:
            pt.Transform(plane_to_world)
            corners[count]=pt
            count+=1
        return corners,bbox
    return corners,bbox
def linecrvintersect(line,curve):
    global tol
    cbox=rh.Geometry.BoundingBox.Empty
    bbox=BoundingBox([curve])[1]
    bool,ends=rh.Geometry.Intersect.Intersection.LineBox(line,bbox,tol)
    linecurve=rh.Geometry.LineCurve(line.PointAt(ends[0]),line.PointAt(ends[1]))
    return linecurve,rh.Geometry.Intersect.Intersection.CurveCurve(linecurve,curve,tol,tol)
def pttopix(point3d):
    return rh.Geometry.Point3d(int(math.floor(point3d[0])),int(math.floor(point3d[1])),0)
def collect_overlaps(curve,curves):
    global tol
    newcurves=[]
    for crv in curves:
        intersection=rh.Geometry.Intersect.Intersection.CurveCurve(curve,crv,tol,tol)
        if intersection.Count>0:
            for i in intersection:
                try:
                    newcurves.append(curve.Trim(i.OverlapA))
                except:
                    pass
    if len(newcurves)>0:
        jcurves=rh.Geometry.Curve.JoinCurves(newcurves)
        return jcurves
    else:
        return newcurves
def dupborder(brep, join=True):
    global tol
    curves=brep.DuplicateNakedEdgeCurves(True,True)
    if not join:
        return curves
    else:
        return rh.Geometry.Curve.JoinCurves(curves,tol*2.1)
def propersolidfromextrusion(badbrep):
    '''badbrep is not that bad but comes from extruding brep face and gives wrong results when inquiring if a point is inside'''
    #sc.doc = rh.RhinoDoc.ActiveDoc#UNCOMMENT IN GH - COMMENT IN RHINO
    k=sc.doc.Objects.AddBrep(badbrep)
    cleanbrep=rhob_to_rcob([k])[0]
    rs.DeleteObject(k)
    #sc.doc = ghdoc#UNCOMMENT IN GH - COMMENT IN RHINO
    return cleanbrep
def remap_parameter_from_curve(nparameter,cbox):
    start=(cbox.bbox[0][0]+((rh.Geometry.Vector3d(cbox.bbox[0][1]-cbox.bbox[0][0]))*nparameter))
    lc=rh.Geometry.LineCurve(start,start+rh.Geometry.Vector3d(cbox.bbox[0][2]-cbox.bbox[0][1]))
    i=rh.Geometry.Intersect.Intersection.CurveCurve(lc,cbox.curve,tol,tol)
    return (i[0].PointA-start).Length / (lc.GetLength())
#------------------------------------------------FUNCTIONS SPECIFIC TO THIS SCRIPT
def getvalgrad(pos,remapcurve,cb=None,linear=False):
    if cb==None:
        col1=col.FromArgb(255,0,0,0)
        col2=col.FromArgb(0,0,0,0)
    else:
        col0,col1=cb[0],cb[1]
    if not linear:
        pos=remap_parameter_from_curve(pos,remapcurve)
    intA=rh.Geometry.Interval(col0.A,col1.A)
    poscolA=int(math.floor(intA.ParameterAt(pos)))
    intR=rh.Geometry.Interval(col0.R,col1.R)
    poscolR=int(math.floor(intR.ParameterAt(pos)))
    intG=rh.Geometry.Interval(col0.G,col1.G)
    poscolG=int(math.floor(intG.ParameterAt(pos)))
    intB=rh.Geometry.Interval(col0.B,col1.B)
    poscolB=int(math.floor(intB.ParameterAt(pos)))
    poscol=col.FromArgb(poscolA,poscolR,poscolG,poscolB)
    return poscol
#------------------------------------------------CLASSES SPECIFIC TO THIS SCRIPT
class curvewithbboxXY(object):
    '''adds bounding box in XY plane to curve'''
    def __init__(self,curve):
        self.curve=curve
        self.bbox=BoundingBox([curve])
class Zoneg(object):
    '''gradient area'''
    def __init__(self,brep,nucl):
        zcrv=rh.Geometry.LineCurve(rh.Geometry.Point3d(0.0,0.0,0.0),rh.Geometry.Point3d(0.0,0.0,1000.0))
        self.brep=brep
        self.pixels=[]
        self.region=brep.Faces[0]
        self.nucl=nucl
        self.nsurface=self.region.UnderlyingSurface()
        self.domainU=self.nsurface.Domain(0)
        nucext=(nucl.Faces[0].CreateExtrusion(zcrv,True))
        solext=(self.region.CreateExtrusion(zcrv,True))
        self.nuclsolid=propersolidfromextrusion(nucext)
        self.solid=propersolidfromextrusion(solext)
    def get_pixels_in(self,remapcurve,colors,linear,height):
        #populates instances of pixels contained in that zone
        global tol
        corners=BoundingBox([self.brep,self.nucl])[0]
        first,last=pttopix(corners[0]),pttopix(corners[2])
        for x in xrange(first[0],last[0]+1):
            distances=set()
            point=rh.Geometry.Point3d(x+0.5,-0.5,500.0)
            boundingpts=rh.Geometry.Intersect.Intersection.ProjectPointsToBreps([self.solid,self.nuclsolid],[point],rh.Geometry.Vector3d.YAxis,tol)
            for bp in boundingpts:
                di=int(math.floor(point.DistanceTo(bp)))
                distances.add(di)
            k=list(distances)
            k.sort()
            for i in xrange(len(k)-1):
                interval=k[i],k[i+1]-1
                y=((interval[0]+interval[1])/2.0)+0.5
                testpoint=rh.Geometry.Point3d(x+0.5,y,500.0)
                flag1=self.solid.IsPointInside(testpoint,tol,False)
                flag2=self.nuclsolid.IsPointInside(testpoint,tol,True)
                if (not flag2) and (not flag1):
                    pass
                else:
                    for j in xrange(interval[0],interval[1]+1):
                        gradpos=0
                        if flag1:
                            tittestpoint=rh.Geometry.Point3d(x+0.5,j+0.5,500.0)
                            pos=(self.nsurface.ClosestPoint(tittestpoint))[1]
                            gradpos=self.domainU.NormalizedParameterAt(pos)
                        picture.SetPixel(x,height-j-1,getvalgrad(gradpos,remapcurve,colors,linear))
    print "job done!"
#---------------------------------FILE INPUT - done quick and dirty
#sc.doc = rh.RhinoDoc.ActiveDoc#UNCOMMENT IN GH - COMMENT IN RHINO
z2=[]
test=rs.ObjectsByLayer('ZONE_II')
testsort2=[(rs.ObjectName(t),t) for t in test]
testsort2.sort()
for name,t in testsort2:
    z2.extend(rhob_to_rcob([t]))
    
z3=[]
test=rs.ObjectsByLayer('ZONE_III')
testsort3=[(rs.ObjectName(t),t) for t in test]
testsort3.sort()
for name,t in testsort3:
    z3.extend(rhob_to_rcob([t]))
    
zGroups=zip(z3,z2)
gradcurve=rs.ObjectsByLayer('curve_g')
gc=rhob_to_rcob(gradcurve)[0] #later we could have different ones each associated with a gradient zone
frame=rs.ObjectsByLayer('Frame')[0]
rframe=rhob_to_rcob(frame)
fc,fb=BoundingBox([rframe])
width=int(math.floor(fc[0].DistanceTo(fc[1])))
height=int(math.floor(fc[0].DistanceTo(fc[3])))
#sc.doc = ghdoc#UNCOMMENT IN GH - COMMENT IN RHINO
if __name__ == '__main__':
    #------------------------------------------------USER INPUT TO MODIFY HERE
    c1=col.FromArgb(255,232,240,17)
    c2=col.FromArgb(128,138,197,81)
    thisdir=os.getcwd()
    filename='test1.png'
    picpath=os.path.join(thisdir,filename)
    linear=False
    #------------------------------------------------CREATE THE BITMAP!
    imformat=imgg.Png
    picture=btmp(width,height)
    Curvewithbbox=curvewithbboxXY(gc)
    for z3,z2 in zGroups:
        z=Zoneg(z2,z3)
        z.get_pixels_in(Curvewithbbox,[c1,c2],linear,height)
    picture.Save(picpath,imformat)

Leave a Comment