In [471]:
%reset -f

Voxel painter in pythreejs

Based on: voxel painter (source)

TODO:

- Have rollOver helper snap to other voxel face
- Delete voxel when shift-clicked

In [472]:
from __future__ import division, print_function

In [473]:
from pythreejs import *
from IPython.display import display
from ipywidgets import HTML
from traitlets import link

In [474]:
import random
import math
import numpy as np

Geometry definitions


In [475]:
csize = 50 # size of voxel
stepx, stepy = csize, csize
sizex, sizey = 10*csize, 10*csize

Helper functions


In [476]:
def normalize(list):
    """ 
    Normalize vector list 
    """
    return [x/sum(list) for x in list]

def rotation_matrix(angle, axis='x'):
    """ 
    Return rotation matrix as list of rows used in the
    Object3d.quaternion_from_rotation() class method
    """
    if axis in ['x','y','z']:
        sin = math.sin(angle)
        cos = math.cos(angle)
        # counter-clockwise rotation in yz-plane
        if axis is 'x':
            return [1, 0, 0, 0, cos, -sin, 0, sin, cos]
        # counter-clockwise rotation in xz-plane
        elif axis is 'y':
            return [cos, 0, sin, 0, 1, 0, -sin, 0, cos]
        # counter-clockwise rotation in xy-plane
        elif axis is 'z':
            return [cos, -sin, 0, sin, cos, 0, 0, 0, 1]
    else:
        raise ValueError('Cannot rotate about %s axis' % axis)

Scene objects:

Voxels


In [477]:
cube_tex = ImageTexture(
    imageuri = 'textures/square-outline-textured.png'
)

cube_geo = BoxGeometry(
    width = csize,
    height = csize,
    depth = csize
)

cube_mat = LambertMaterial(
    color = 0xfeb74c,
    shading = 'FlatShading',
    map = cube_tex
)

Roll-over helper


In [478]:
rollOver_geo = cube_geo

rollOver_mat = BasicMaterial(
    color = 0xff0000,
    opacity = 0.5,
    transparent = True
)

rollOver_point = Mesh(
    geometry = rollOver_geo,
    material = rollOver_mat
)

Surface


In [479]:
surf_geo = SurfaceGeometry(
    z = [0]*sizex*sizey,
    width = 2*sizex,
    height = 2*sizey,
    width_segments = 2*sizex//stepx,
    height_segments = 2*sizey//stepy,
)

surf_grid = SurfaceGrid(
    geometry = surf_geo,
    material = LineBasicMaterial(
        color = 0x000000,
        opacity = 0.2,
        transparant = True
    ),
)

surface = Mesh(
    geometry = surf_geo,
    material = BasicMaterial(
        color = 'red',
        opacity = 0.2,
    ),
    visible = False
)

m = rotation_matrix(-math.pi/2)
surface.quaternion_from_rotation(m)
surf_grid.quaternion_from_rotation(m)

Pickers (raycasting)


In [480]:
click_picker = Picker(
    root = surface,
    event = 'click'
)
mousemove_picker = Picker(
    root = surface,
    event = 'mousemove'
)

In [481]:
def map_to_grid(value):
    """
    Convert continous to discrete coordinates
    """
    # limit position to positive y-axis
    if value[1] < 0: 
        value[1] = float(0)
        
    # limit to discrete steps based on cube size
    pos = [int(x//csize*csize+csize/2) for x in value]

    # if block already exist at this position, shift up
    while tuple(pos) in objects.keys():
        pos[1] += csize
    return pos

In [482]:
objects = {} # contains all voxels added to the scene
def on_click(name, value):
    """
    Create new object when mouse is clicked
    TODO: delete when shift-clicked
    """
    # convert position to discrete coordinates
    pos = map_to_grid(value)
        
    # create new object
    point = Mesh(
        geometry = cube_geo,
        material = cube_mat,
        position = pos
    )
    
    # add new object to scene and object list
    scene.children = scene.children + [point] #works
    #scene.children.append(point) # doesnt work (see: https://github.com/jovyan/pythreejs/blob/master/pythreejs/pythreejs.py#L134)
    objects[tuple(map(int,point.position))] = point

In [483]:
html = HTML()
def on_mousemove(name, value):
    """
    Show rollOver helper on mousemove
    TODO: Snap rollOver helper to existing voxel
    """
    # convert to discrete coordinates
    pos = map_to_grid(value)
    
    # update rollOver helper position
    rollOver_point.position = pos
    
    # write coordinates to html container
    html.value = "Coords: (%d, %d, %d)" % tuple(pos)    

# initialize with starting position
on_mousemove(None, rollOver_point.position)

In [484]:
click_picker.on_trait_change(on_click, 'point')
link((rollOver_point, 'position'), (mousemove_picker, 'point'))
mousemove_picker.on_trait_change(on_mousemove, 'point')

Camera and scene


In [485]:
camera = PerspectiveCamera(
    position = [500, 800, 1300],
    fov = 35, 
    aspect = 16/10,
    near = 1,
    far = 10000
)
camera.look_at(camera.position, [0,0,0])

In [486]:
scene = Scene(
    children = [
        surface,
        surf_grid,
        rollOver_point,
        AmbientLight(
            color = 0x606060
        ),
        DirectionalLight(
            color = 0xffffff,
            position = normalize([1, 0.75, 0.5]),
            intensity = 0.5
        )
    ]
)

Render the scene


In [487]:
renderer = Renderer(
    camera = camera,
    scene = scene,
    controls = [
        OrbitControls(
            controlling = camera
        ),
        click_picker,
        mousemove_picker
    ],
    background = 0xf0f0f0,
    antialias = True,
    renderer_type = 'auto' #'auto', 'canvas', 'webgl'
)

In [488]:
display(html, renderer)