In [1]:
%%javascript
requirejs.config({
  paths: {
        'threejs': 'http://boxen.math.washington.edu/home/jason/three.js/build/three.min',
        'threejs-trackball': 'http://boxen.math.washington.edu/home/jason/three.js/examples/js/controls/TrackballControls',
        'threejs-orbit': 'http://boxen.math.washington.edu/home/jason/three.js/examples/js/controls/OrbitControls',
        'threejs-detector': 'http://boxen.math.washington.edu/home/jason/three.js/examples/js/Detector',
  },
  shim: {
    'threejs': {exports: 'THREE'},
    'threejs-trackball': {exports: 'THREE.TrackballControls',
                          deps: ['threejs']},
    'threejs-orbit': {exports: 'THREE.OrbitControls',
                      deps: ['threejs']},
    'threejs-detector': {exports: 'Detector'},
  },
    waitSeconds: 20,
});
// require three.js so it is globally available
require(["threejs-trackball", "threejs-orbit", "threejs-detector"], function() {console.log('three.js loaded')});



In [1]:


In [15]:
%%javascript

require(["notebook/js/widgets/widget"], function() {

    var RendererView = IPython.DOMWidgetView.extend({
        render : function(){
            console.log('created renderer');
            var width = this.model.get('width');
            var height = this.model.get('height');
            if ( Detector.webgl )
                this.renderer = new THREE.WebGLRenderer( {antialias:true} );
            else
                this.renderer = new THREE.CanvasRenderer(); 
            this.renderer.setSize( width, height);
            this.$el.empty().append( this.renderer.domElement );
            this.camera = this.child_view(this.model.get('camera'));
            this.scene = this.child_view(this.model.get('scene'));
            this.scene.obj.add(this.camera.obj);
            console.log('renderer', this.model, this.scene.obj, this.camera.obj);
            this.update();
            var that = this;
            this.controls = this.child_view(this.model.get('controls'), {dom: this.renderer.domElement, 
                                                                         update: function(fn, context) {
                                                                                    that.on('render:update', fn, context);
                                                                                 }
                                                                        })
            this.animate();
            window.r = this;
        },
        animate: function() {
            requestAnimationFrame( _.bind(this.animate, this) );
            this.trigger('render:update');
            this.renderer.render(this.scene.obj, this.camera.obj);
        },
        update : function(){
            console.log('update renderer', this.scene.obj, this.camera.obj);
            return IPython.DOMWidgetView.prototype.update.call(this);
        },        
    });

    IPython.widget_registry.register_widget_view('RendererView', RendererView);
    
    var ThreeView = IPython.WidgetView.extend({       
        replace_obj: function(new_obj) {
            var old_obj = this.obj;
            this.obj = new_obj;
            this.trigger('replace_obj', old_obj, new_obj);
        }
    });

    
    var Object3dView = ThreeView.extend({
        // this is meant to be called *after* the object has been created, and modifies the object
        // to reflect rotation, matrix, etc.
        update_object_parameters: function() {
            var array_props = ['position', 'rotation', 'up', 'scale']
            for (var prop=0,len=array_props.length; prop<len; prop++) {
                var p = array_props[prop];
                if (p !== null) {
                    this.obj[p].fromArray(this.model.get(p));
                }
            }
            var bool_props = ['visible', 'castShadow', 'receiveShadow']
            for (var prop=0,len=bool_props.length; prop<len; prop++) {
                var p = bool_props[prop]
                this.obj[p] = this.model.get(p);
            }
            if (this.model.get('matrix').length===16) {
                this.obj.matrix.fromArray(this.model.get('matrix'));
            }
        },
        
        update_children: function(oldchildren, newchildren) {
            // TODO: be smarter about only removing and adding children that are actually changed
            for (var obj in this.obj.children) {
                this.obj.remove(obj);
            }
            this.update_child_views(oldchildren, newchildren);
            _.each(newchildren, function(element, index, list) {
                var child_view = this.child_views[element];
                this.obj.add(child_view.obj);
                // TODO: we could be smarter about how we register or deregister for events
                child_view.off('replace_obj', null, this);
                child_view.on('replace_obj', this.replace_child_obj, this)
            }, this)
        },

        render: function() {
            this.obj = this.new_obj();
            this.update_children([], this.model.get('children'));
            this.update();
        },
        new_obj: function() {
            return new THREE.Object3D();
        },
        update: function() {
            //this.replace_obj(this.new_obj());
            this.update_object_parameters();
            if (this.model.hasChanged('children')) {
                this.update_children(this.model.previous('children'), this.model.get('children'));
            }
        },
        replace_child_obj: function(old_obj, new_obj) {
            this.obj.remove(old_obj);
            this.obj.add(new_obj);
            // TODO: trigger re-render, when we have an event-driven rendering loop
        },
    });
    IPython.widget_registry.register_widget_view('Object3dView', SceneView);
    
    var CameraView = Object3dView.extend({
        new_obj: function() {
            return new THREE.PerspectiveCamera( this.model.get('fov'), this.model.get('ratio'), 1, 1000 );
        }
    });
    IPython.widget_registry.register_widget_view('CameraView', CameraView);

    var OrbitControlsView = ThreeView.extend({
        render: function() {
            this.controlled_view = this.model.widget_manager.get_model(this.model.get('controlling')).views[0];
            this.obj = new THREE.OrbitControls(this.controlled_view.obj, this.options.dom);
            this.options.update(this.obj.update, this.obj);
            delete this.options.renderer; 
        }
    });
    IPython.widget_registry.register_widget_view('OrbitControlsView', OrbitControlsView);


    var SceneView = Object3dView.extend({
        render: function() {
            var scene = this.obj = new THREE.Scene();
            this.update_children([], this.model.get('children'))
            this.update();
            return scene;
        }    
    });
    IPython.widget_registry.register_widget_view('SceneView', SceneView);

    var SurfaceGeometryView = ThreeView.extend({
        render: function() {
            this.update()
            return this.obj;
        },
        update: function() {
            var obj = new THREE.PlaneGeometry(this.model.get('width'),
                                              this.model.get('height'),
                                              this.model.get('width_segments'),
                                              this.model.get('height_segments'));
            // PlaneGeometry constructs its vertices by going across x coordinates, starting from the maximum y coordinate
            var z = this.model.get('z');
            for (var i = 0, len = obj.vertices.length; i<len; i++) {
                obj.vertices[i].z = z[i];
            }
            obj.computeCentroids()
            obj.computeFaceNormals();
            obj.computeVertexNormals();
            this.replace_obj(obj);
        }
    });
    IPython.widget_registry.register_widget_view('SurfaceGeometryView', SurfaceGeometryView);
    
    var SphereGeometryView = ThreeView.extend({
        render: function() {
            this.update()
            return this.obj;
        },
        update: function() {
            this.replace_obj(new THREE.SphereGeometry(this.model.get('radius'), 32,16));
        }
    })
    IPython.widget_registry.register_widget_view('SphereGeometryView', SphereGeometryView);

    
    var MaterialView = ThreeView.extend({
        render: function() {
            this.obj = new THREE.MeshLambertMaterial({color: this.model.get('color'), 
                                                      side: THREE.DoubleSide});
            return this.obj;
        },
        update: function() {
            this.obj.color.set(this.model.get('color'));
            this.obj.opacity = this.model.get('opacity');
            this.obj.transparent = (this.obj.opacity<1.0);
            this.obj.wireframe = this.model.get('wireframe');
            this.obj.needsUpdate=true;
        }
    })
        IPython.widget_registry.register_widget_view('MaterialView', MaterialView);

    var MeshView = Object3dView.extend({
        render: function() {
            this.geometryview = this.child_view(this.model.get('geometry'));
            this.materialview = this.child_view(this.model.get('material'));
            this.geometryview.on('replace_obj', this.update, this);
            this.materialview.on('replace_obj', this.update, this);
            this.update()
            return this.obj;
        },
        update: function() {
            this.replace_obj(new THREE.Mesh( this.geometryview.obj, this.materialview.obj ));
            //this.obj.geometry = this.geometryview.obj;
            //this.obj.material = this.materialview.obj;
            //this.obj.material.needsUpdate=true;
            Object3dView.prototype.update.call(this);
        }        
    });   
        IPython.widget_registry.register_widget_view('MeshView', MeshView);
    
    
        var Basic3dObject = Object3dView.extend({
        render: function() {
            this.update()
            return this.obj;
        },
        update: function() {
            this.replace_obj(this.new_obj());
            Object3dView.prototype.update.call(this);
        }
    });
    var AmbientLight = Basic3dObject.extend({
        new_obj: function() {
            return new THREE.AmbientLight(this.model.get('color'));
        }
    });   
    IPython.widget_registry.register_widget_view('AmbientLight', AmbientLight);
    
    var DirectionalLight = Basic3dObject.extend({
        new_obj: function() {
            return new THREE.DirectionalLight(this.model.get('color'), this.model.get('intensity'));
        }
    });   
    IPython.widget_registry.register_widget_view('DirectionalLight', DirectionalLight);

    var PointLight = Basic3dObject.extend({
        new_obj: function() {
            return new THREE.PointLight(this.model.get('color'), 
                                        this.model.get('intensity'),
                                        this.model.get('distance'));
        }
    });   
    IPython.widget_registry.register_widget_view('PointLight', PointLight);
    
    var SpotLight = Basic3dObject.extend({
        new_obj: function() {
            return new THREE.SpotLight(this.model.get('color'), 
                                        this.model.get('intensity'),
                                        this.model.get('distance'));
        }
    });   
    IPython.widget_registry.register_widget_view('SpotLight', SpotLight);

});



In [2]:


In [2]:


In [9]:
# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets.widget import Widget, DOMWidget
from IPython.utils.traitlets import (Unicode, Int, Instance, Enum, List, Float, 
                                     Any, CFloat, Bool, This, CInt)
import numpy

def vector3(trait_type, default=None):
    if default is None: 
        default=[0,0,0]
    return List(trait_type, default_value=default, 
                minlen=3, maxlen=3, allow_none=False)


class Object3d(Widget):
    """
    If matrix is not None, it overrides the position, rotation, scale, and up variables.
    """
    keys = ['position', 'rotation', 'scale', 'up', 
             'visible', 'castShadow', 'receiveShadow', 
             'matrix',
             'children'] + Widget.keys
    position = vector3(CFloat)
    rotation = vector3(CFloat)
    scale = vector3(CFloat, [1,1,1])
    up = vector3(CFloat, [0,1,0])
    visible = Bool(True)
    castShadow = Bool(False)
    receiveShadow = Bool(False)
    matrix = List(CFloat)
    # TODO: figure out how to get a list of instances of Object3d
    children = List(trait=None, default_value=[], allow_none=False)

class Controls(Widget):
    view_name = Unicode('ControlsView')
    keys = ['controlling'] + Widget.keys
    controlling = Instance(Object3d)

class OrbitControls(Controls):
    view_name = Unicode('OrbitControlsView')
    
class Geometry(Widget):
    view_name = Unicode('GeometryView')

class SphereGeometry(Geometry):
    view_name = Unicode('SphereGeometryView')
    keys = ['radius'] + Geometry.keys
    radius = CFloat(1)

class SurfaceGeometry(Geometry):
    """
    A regular grid with heights
    """
    view_name = Unicode('SurfaceGeometryView')
    keys = ['z', 'width', 'height', 'width_segments', 'height_segments'] + Geometry.keys
    z = List(CFloat, [0]*100)
    width = CInt(10)
    height = CInt(10)
    width_segments = Int(10)
    height_segments = Int(10)

class Material(Widget):
    view_name = Unicode('MaterialView')
    keys = ['color', 'opacity', 'wireframe'] + Widget.keys
    color = Any('yellow')
    opacity = CFloat(1.0)
    wireframe = Bool(False)

class Mesh(Object3d):
    view_name = Unicode('MeshView')
    keys = ['geometry', 'material'] + Object3d.keys
    geometry = Instance(Geometry)
    material = Instance(Material)

class Camera(Object3d):
    view_name = Unicode('CameraView')
    keys = ['fov', 'ratio'] + Object3d.keys
    fov = CFloat(40)
    ratio = Float(600.0/400.0)
    
class Scene(Object3d):
    view_name = Unicode('SceneView') 
    
class Renderer(DOMWidget):
    view_name = Unicode('RendererView')
    keys = ['width', 'height', 'renderer_type', 'scene', 'camera', 'controls'] + DOMWidget.keys
    width = Int(600)
    height = Int(400)
    renderer_type = Enum(['webgl', 'canvas', 'auto'], 'auto')
    scene = Instance(Scene)
    camera = Instance(Camera)
    controls = Instance(Controls)

In [10]:
class Light(Object3d):
    keys = ['color']+Object3d.keys
    color = Any('white') # could be string or number or tuple

class AmbientLight(Light):
    view_name = 'AmbientLight'

class PositionLight(Light):
    keys = ['intensity'] + Light.keys
    view_name = 'PositionLight'
    intensity = CFloat(1)
    
class DirectionalLight(PositionLight):
    view_name = 'DirectionalLight'

class PointLight(PositionLight):
    keys = ['distance'] + PositionLight.keys
    view_name = 'PointLight'
    distance = CFloat()

class SpotLight(PointLight):
    keys = ['angle', 'exponent'] + PointLight.keys
    view_name = 'SpotLight'
    angle = CFloat()
    exponent = CFloat()
    

lights = {
    'colors': [
        AmbientLight(color=(0.312,0.188,0.4)),
        DirectionalLight(position=[1,0,1], color=[0.8, 0, 0]),
        DirectionalLight(position=[1,1,1], color=[0, 0.8, 0]),
        DirectionalLight(position=[0,1,1], color=[0, 0, 0.8]),
        DirectionalLight(position=[-1,-1,-1], color=[.9,.7,.9]),
        ],
    'shades': [
        AmbientLight(color=[.6, .6, .6]),
        DirectionalLight(position=[0,1,1], color=[.5, .5, .5]),
        DirectionalLight(position=[0,0,1], color=[.5, .5, .5]),
        DirectionalLight(position=[1,1,1], color=[.5, .5, .5]),
        DirectionalLight(position=[-1,-1,-1], color=[.7,.7,.7]),
        ],
    }

In [4]:


In [5]:
from IPython.display import display
from IPython.utils.traitlets import Connect
from IPython.html.widgets import FloatRangeWidget
import numpy as np
nx,ny=(20,20)
xmax=1
x = np.linspace(-xmax,xmax,nx)
y = np.linspace(-xmax,xmax,ny)
xx, yy = np.meshgrid(x,y)
z = xx**2-yy**2
surface = SurfaceGeometry(z=list(z[::-1].flat), 
                          width=2*xmax,
                          height=2*xmax,
                          width_segments=nx-1,
                          height_segments=ny-1)
ball = SphereGeometry(radius=.3)
ballmaterial = Material(color=0x00cc00)

sphere = Mesh(geometry=ball, material=ballmaterial, position=[1,1,1])
plane = Mesh(geometry=surface, material=Material(color=0xcccc00))
spotlight = SpotLight(color=0xffffff, position=[0,0,1])
c = Camera(position=[0,10,10], children=[spotlight], fov=40)
scene = Scene(children=[sphere, 
                        plane,
                        AmbientLight(color=0xaaaaaa)
                        ])

renderer = Renderer(camera=c, scene = scene, controls=OrbitControls(controlling=c))
f=FloatRangeWidget(min=0.1,max=10)
g=FloatRangeWidget(view_name='FloatTextView')
Connect((f,'value'),(g,'value'),(ball, 'radius'))
display(f)
display(g)
display(renderer)

In [15]:


In [6]:
# we can dynamically change properties
ball.radius=0.5
ballmaterial.color='purple'
ballmaterial.opacity=0.8
sphere.position=[0,1,0]
z = xx**2+np.sin(yy**2)
surface.z = list(z[::-1].flat)
c.position=[1,2,3]

In [7]:
import time, math
ballmaterial.color = 0x4400dd
for i in range(1,200,2):
    ball.radius=i/100.
    ballmaterial.color +=0x000300
    ballmaterial.opacity+=0.05
    sphere.position = [math.cos(i/100.), math.sin(i/50.), i/100.]
    time.sleep(.1)

In [14]:


In [ ]:


In [ ]:

How do we deal with the trackball? On the one hand, it is changing the camera, but on the other hand, it is tied to the renderer DOM element.

I think the controllers should be subobjects of any Object3D (including a camera). An object3d should check some properties like position, matrix, controller, etc., and always act on those. For a controller, the object3d should pass a view argument of its own three.js component. The controller view will expect an argument of some object to control.

But then somehow the controller's dom element will need to be set to the renderer's dom element. It can be done after the fact. Perhaps a list of props from the original renderer can be shared, including the dom element? Just like in react.js, a list of props can be passed down from the renderer view through the subviews.

Another option is to add the controller to the renderer (where it will have access to the renderer DOM), and add as an option to the controller the model it is controlling. The controller would then not render the view, but look up the view that has already been rendered, and control that matrix.

OR perhaps the controller should control a generic object3d, and we should use the backbone system to hook the controller matrix and the controllee matrix together. the nice thing about this is that it is easy to call the control update method in the render update.

Or perhaps anything that needs to update before a render should register an event handler (check to make sure the event handlers are done synchronously rather than asynchronously). Then the control should just listen to the renderer update event.

Perhaps the renderer can also listen to the renderer.set_controller_element event. When the event is triggered with the controller, the renderer can set the controller's dom element and call the resize method.

final thinking

I thought more about it:

  1. [x] the controller should be a member of the renderer model
  2. [x] we should be able to pass arguments to "child_view", which will be passed on to the view. The renderer will pass its dom element as an argument to the controller.
  3. [x] when a view for the controller is created, it will look up the model it is controlling get the first view if the view has been created, otherwise it will create the view. (another way to do it is just throw an error, but any three.js object, it makes sense to just have a 1-1 correspondence between views and models, since the model is representing the state of one specific three.js object).
  4. [x] another view argument should be some way of listening to renderer view events; specifically, listening to the render:update event, which will be triggered in the middle of a render update. this is a time for the controls to update themselves, any models which are actually animations to update themselves, etc. So this callback should be passed down the entire tree in the view arguments.
  5. we should also have a way for events in the views to bubble up. For example, a request to rerender could be an event which, when it bubbles up to the renderer level, sets a flag and starts a renderAnimation loop if one isn't already going. This is an optimization in the renderAnimation loop---perhaps we should just have the renderAnimation loop going constantly at first.
  6. [x] Another bubble-up event could be if a geometry three.js object in a view is actually replaced---it will want to notify its mesh parent not just to rerender, but to actually recreate a new mesh, so it will trigger a replaced event with the old geometry as an argument. So the mesh could listen to a "replace" event and recreate the mesh with the new geometry, stop the bubbling, and bubble up its own replace event with the old three.js object as an argument. the scene would catch this event, remove the old object and add the new object (and trigger a rerender event), and then stop the bubbling. Actually, do we need full bubbling for this? Probably not---the immediate parent needs to decide whether to take action, or do something and propogate.

TODO

  • [X] Make Object3d class, with position, matrix, etc.
  • [X] Fix mousewheel bug
  • [X] events registering changes to meshes or geometries
  • [ ] Add wireframes to meshes (not just set mesh materials to wireframe)
  • [ ] light python objects
  • [ ] fix ipython picking bug (clicking on the output of a cell jumps the cursor to the input of the cell, which is annoying when you are trying to interact with the output)
  • [ ] three.js picking

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]: