Pixelate3D

Hi, everyone.

Here’s my “version 1.0” Pixelation script. Also attached are two images showing the basic functionality.

Of course, Suzanne isn’t the best example - cambo mentioned that it is not a closed mesh because the eyes are non manifold. But the script mostly works for Suzanne. For any fully closed mesh, however, it should work without any errors.

The GUI allows you to choose the “brick” sizing in all three axes, to choose the destination layer for the bricks, and whether or not you want the original object removed.

Note that it can be quite slow. I received a few suggestions from cambo for speed ups that I’ll look into next.

Some of the key parts of this code are from cambo, with minor/tiny modifications.

BEFORE:
http://www.curiouskangaroo.com/monkey1.jpg

AFTER:
http://www.curiouskangaroo.com/monkey2.jpg


#!BPY

"""
Name: 'Pixelate'
Blender: 241
Group: 'Mesh'
Tooltip: '3D Pixelation of Meshes'
"""

__author__ = "David Levine"
__url__ = ("blender", "elysiun")
__version__ = "1.0 02/24/06"

__bpydoc__ = """\
This script takes closed meshes and 'pixelates' them in 3D.

Usage:

Select the mesh and run this script.  You can specify the size of
the blocks used.  You can specify the destination layer for those
blocks.  You can also specify that the original object is removed.
Upon hitting the PIXELATE button, a 'blocky' version of your original
mesh will be created.

Thanks to: cambo, RipSting, JM Soler
"""

from Blender import *
from Blender.BGL import *
from Blender.Draw import *
from Blender.Noise import *

def ptInFaceXYBounds(f, pt):
    co= f.v[0].co
    xmax= xmin= co.x
    ymax= ymin= co.y

    co= f.v[1].co
    xmax= max(xmax, co.x)
    xmin= min(xmin, co.x)
    ymax= max(ymax, co.y)
    ymin= min(ymin, co.y)

    co= f.v[2].co
    xmax= max(xmax, co.x)
    xmin= min(xmin, co.x)
    ymax= max(ymax, co.y)
    ymin= min(ymin, co.y)

    if len(f.v)==4: 
        co= f.v[3].co
        xmax= max(xmax, co.x)
        xmin= min(xmin, co.x)
        ymax= max(ymax, co.y)
        ymin= min(ymin, co.y)
        
    # Now we have the bounds, see if the point is in it.
    if\
    pt.x < xmin or\
    pt.y < ymin or\
    pt.x > xmax or\
    pt.y > ymax:
        return False # point is outside face bounds
    else:
        return True # point inside.
        
def pointInsideMesh(ob, pt):
    obMat = Mathutils.Matrix(ob.matrixWorld)
    obInvMat = obMat.invert()
    obSpaceVec = pt* obInvMat
    obSpacePt = Mathutils.Vector(obSpaceVec[0], obSpaceVec[1], obSpaceVec[2])

    Intersect = Mathutils.Intersect # 2 less dict lookups.
    Vector = Mathutils.Vector

    ray = Vector(0,0,-1)

    def faceIntersect(f):
        isect= Intersect(f.v[0].co, f.v[1].co, f.v[2].co, ray, obSpacePt, 1) # Clipped.
        if not isect and len(f.v) == 4:
            isect= Intersect(f.v[0].co, f.v[2].co, f.v[3].co, ray, obSpacePt, 1) # Clipped.

        if isect and isect.z > obSpacePt.z: # This is so the ray only counts if its above the point. 
            return True
        else:
            return False

    me= ob.getData(mesh=1)

    # Here we find the number on intersecting faces, return true if an odd number (inside), false (outside) if its true.
    return len([None for f in me.faces if ptInFaceXYBounds(f, obSpacePt) if faceIntersect(f)]) % 2

def findminmaxxyz(obj):
    minx= 999999999
    maxx= -999999999
    miny= 999999999
    maxy= -999999999
    minz= 999999999
    maxz= -999999999
    boundBox= obj.getBoundBox()
    for v in boundBox:
        minx= min(minx, v.x)
        maxx= max(maxx, v.x)
        miny= min(miny, v.y)
        maxy= max(maxy, v.y)
        minz= min(minz, v.z)
        maxz= max(maxz, v.z)
    minV= Mathutils.Vector(minx,miny,minz)
    maxV= Mathutils.Vector(maxx,maxy,maxz)
    return [minV,maxV]

def makeblock(name='blockModel',mat=None,min=[-1.0,-1.0,-1.0],max=[1.0,1.0,1.0]):
    vertices_list=[
        [min[0],min[1],min[2]],
        [min[0],max[1],min[2]],
        [max[0],max[1],min[2]],
        [max[0],min[1],min[2]],
        [min[0],min[1],max[2]],
        [min[0],max[1],max[2]],
        [max[0],max[1],max[2]],
        [max[0],min[1],max[2]]
    ]
    faces_list=[
        [0,1,2,3],
        [4,5,6,7],
        [0,4,7,3],
        [1,2,6,5],
        [0,1,5,4],
        [3,7,6,2]
    ]
    blockobj= Object.New('Mesh',name)
    Scene.getCurrent().link(blockobj)
    blockobj.layers = [NumberL.val]
    blockmesh= blockobj.getData()
    for v in vertices_list:
        vert= NMesh.Vert(v[0],v[1],v[2])
        blockmesh.verts.append(vert)
    for f in faces_list:
        meshface= NMesh.Face()
        for vv in f:
            meshface.append(blockmesh.verts[vv])
        blockmesh.faces.append(meshface)
    if mat:
        blockmesh.addMaterial(mat)
    blockmesh.update()
    return blockobj

def faceCent(f):
    x= y= z= 0
    for v in f.v:
        x+= v.co[0]
        y+= v.co[1]
        z+= v.co[2]
    return Mathutils.Vector([x/len(f.v), y/len(f.v), z/len(f.v)])

def nearestFace(ob, pt):
    obMat= Mathutils.Matrix(ob.matrixWorld)
    obInvMat= obMat.invert()
    obSpaceVec= pt* obInvMat
    obSpacePt= Mathutils.Vector(obSpaceVec[0], obSpaceVec[1], obSpaceVec[2])
    mesh= ob.getData()
    closedist= 999999999
    for f in mesh.faces:
        fc = faceCent(f)
        dist = obSpacePt - fc
        if (dist.length < closedist):
            closedist = dist.length
            closeface = f
    return closeface

def pix():
    editmode= Window.EditMode()
    if editmode:
        Window.EditMode(0)
    Window.WaitCursor(1)
    Window.DrawProgressBar(0.0, '')
    count= 1
    spacingx= NumberX.val
    spacingy= NumberY.val
    spacingz= NumberZ.val
    objectList= Object.GetSelected()
    for obj in objectList:
        if obj.getType() == 'Mesh':
            minmax= findminmaxxyz(obj)
            x= minmax[0].x
            while x < (minmax[1].x - (spacingx/4.0)):
                progress= (x - minmax[0].x) / (minmax[1].x - minmax[0].x)
                Window.DrawProgressBar(progress, "Pixelating...")
                y= minmax[0].y
                while y < (minmax[1].y - (spacingy/4.0)):
                    z= minmax[0].z
                    while z < (minmax[1].z - (spacingz/4.0)):
                        posvec= Mathutils.Vector([x + (spacingx/2.0),y + (spacingy/2.0),z + (spacingz/2.0), 1.0])
                        if pointInsideMesh(obj, posvec):
                            nearFaceMaterial= nearestFace(obj, posvec).materialIndex
                            if len(obj.getData().getMaterials()) > 1:
                                nextblock= makeblock('block' + str(count),obj.getData().getMaterials()[nearFaceMaterial],Mathutils.Vector([x,y,z]),Mathutils.Vector([x + spacingx,y + spacingy,z + spacingz]))
                            else:
                                nextblock= makeblock('block' + str(count),None,Mathutils.Vector([x,y,z]),Mathutils.Vector([x + spacingx,y + spacingy,z + spacingz]))
                            count= count + 1
                        z= z + spacingz
                    y= y + spacingy
                x= x + spacingx
    if Replace.val == 1:
        Scene.getCurrent().unlink(obj)
    Window.DrawProgressBar(1.0, '')
    Window.WaitCursor(0)
    if editmode:
        Window.EditMode(1)
    Window.RedrawAll()

Replace= Create(0)
NumberZ= Create(0.20)
NumberY= Create(0.20)
NumberX= Create(0.20)
NumberL= Create(1)

EVENT_NOEVENT = 1
EVENT_PIXELATE = 2

def draw():
    global Button, Replace, NumberZ, NumberY, NumberX, NumberL
    global EVENT_NOEVENT, EVENT_PIXELATE
    glClearColor(0.753, 0.753, 0.753, 0.0)
    glClear(GL_COLOR_BUFFER_BIT)
    glColor3f(0.000, 0.000, 0.000)
    glRasterPos2i(108, 54)
    Text('Select this toggle to completely replace the original mesh.')
    glRasterPos2i(108, 86)
    Text('Z Spacing (0.05 to 10.00)')
    glRasterPos2i(108, 118)
    Text('Y Spacing (0.05 to 10.00)')
    glRasterPos2i(108, 150)
    Text('X Spacing (0.05 to 10.00)')
    glRasterPos2i(108, 182)
    Text('Layer for New Pixels (1 to 20)')
    Button('Pixelate!', EVENT_PIXELATE, 16, 10, 83, 23, '')
    Replace= Toggle('Replace', EVENT_NOEVENT, 16, 42, 83, 23, Replace.val, '')
    NumberZ= Number('', EVENT_NOEVENT, 16, 74, 83, 23, NumberZ.val, 0.05, 10.05, '')
    NumberY= Number('', EVENT_NOEVENT, 16, 106, 83, 23, NumberY.val, 0.05, 10.05, '')
    NumberX= Number('', EVENT_NOEVENT, 16, 138, 83, 23, NumberX.val, 0.05, 10.05, '')
    NumberL= Number('', EVENT_NOEVENT, 16, 170, 83, 23, NumberL.val, 1, 20, '')

def event(evt, val):
    if (evt == QKEY and not val): Exit()

def bevent(evt):
    if evt == EVENT_PIXELATE:
        pix()
    Redraw()

Register(draw, event, bevent)

Great fun - if you add a cylinder on top of all the blocky “pixels”(as an option) you’ll have a “3D Lego-lizer” :smiley:

Tin2tin

Hey, wow this is cool!
P

Nice ! Hehehe… But I have error in console and it hasn’t worked in Blender 2.41:

SyntaxError: invalid token
File “Pixelate.py”, line 58
if
^
SyntaxError: invalid token

P.S. I have installed Python 2.4 and Win XP Pro.

you can delete all the '' at the end of a line and move the next line here. i.e.


if\ 
    pt.x < xmin or\ 
    pt.y < ymin or\ 
    pt.x > xmax or\ 
    pt.y > ymax: 

to


if pt.x < xmin or pt.y < ymin or pt.x > xmax or pt.y > ymax: 

THX Oyster !!! Now work :slight_smile: !

curiouskangaroo <- You should improve your code so as Oyster showed !

Greetings

no, no, that is not a fault of curiouskangaroo, hehe

:o I don’t understand… ??? Why it doesn’t work earlier on my PC ??? Strange…

Very cool script! Thanks. 8)

Thanks for the responses, everyone!

Yes, this had occurred to me. Actually, that was my original goal, but I decided to make the script a little more general. For example, single-stud Lego bricks aren’t perfect cubes but are slightly taller. But since you can adjust the size of the bricks in the GUI, you can account for that. The other thing, of course, is that building a LEGO model entirely out of single-stud bricks is pretty inefficient. I’m working on an update to use larger bricks where possible. The final challenge would be to convert the model colors to their closest LEGO-equivalents, or perhaps even do some three-dimensional dithering.

I’m afraid I can’t help you here. The backslash is a valid continuation character in Python (I think… this was my first project in Python), and the script works as posted on my machine.

When you will use Oysters code then this script will use on my and on your computers !!! :slight_smile: Try !!! If You don’t change code maybe somebody else will have same trouble as I…

It would be interesting to…

  • Have random block rotation.
  • Use blocks that have a distance between eachother. could do some quasi volumetric stuff.
  • Make the script use linked duplicate objects. so the user could select 2 objects. the active would be the pixelate mesh. The inactive would be used for the pixel.

hah. Now Im thinking of particles for bbrush :slight_smile:

One thing you should check is that there is no space character after the backslash in your copy of the script. That prevents Python from interpreting it as a valid line-continuation character. The backslash must be the LAST character on the line.

Hi, cambo. I like #1 and #3 as options - would be fun. Can you explain #2 a bit more? I’m not sure I follow when you talk about blocks having a distance between each other. Do you mean have interstitial spaces between the blocks?

Hi, cambo. I like #1 and #3 as options - would be fun. Can you explain #2 a bit more? I’m not sure I follow when you talk about blocks having a distance between each other. Do you mean have interstitial spaces between the blocks?[/quote]

Imagine you have blocks filling a mesh. and each block is quite small- like a bit of dust, you could then render and it would look like fog/smoke of there were enough. the thicker areas of the object would be more dense.

Some randomization would help the effect look less patterned. (like dithering)

  • You would need to experement with different materials and brush sizes- but you could definetly do some cool stuff with transperent materials also.

wow, interesting.