In [1]:
%%html
<script src="https://rawgithub.com/mrdoob/three.js/master/build/three.js"></script>


Basic design

Python

  • Any time an object is "added" to another object, some root change handler is called that adds the object and all children to the renderer's list of objects.
  • If a javascript renderer object exists:
    • create, update, and delete change handlers are added to each object, to forward messages over the renderer's channel
    • , the new objects are forwarded to javascript.
  • The renderer adds change handlers to each new object that will forward changes to the javascript side. The renderer subscribes to change events for all of the additions' model keys.
  • A threejs python object walks the tree and stores both a hash of each object, where instead of nested submodels, there are just references to subobject ids. For example:
    • id: {type: 'renderer', scene: id, camera: id}
    • id: {type: 'camera', }
    • id: {id: id, as well as an object graph of the form:{camera: id, scene: {meshes: [id, id, id]}

Each Comm

  • every object has a comm to its counterpart model in javascript

Here is an example

  • a python mesh is instantiated, along with geometry and texture objects
  • the python mesh is added to the scene object, and a unique id is assigned to it
  • if the scene is not displayed (i.e., no comm is initialized), do nothing
  • if the scene is displayed, then for the root object (recursively) (could do this top down or bottom-up. we choose bottom-up, which means the parent object needs to add the children)
    • create the comms for each of the subobjects, and render them
    • create the root comm, along with references to the comm ids for each of the children. From the children comm ids, retrieve the children models and store a reference to them. Then render the node (which creates the three.js object)
    • have the scene update its model to include a reference to the new model and update it (which adds the root node to the scene)

In [2]:
%%javascript
require(["notebook/js/widget"], function() {

    var GeometryModel = Backbone.Model.extend({});
    IPython.widget_manager.register_widget_model('GeometryModel', GeometryModel);

    var SphereGeometryModel = Backbone.Model.extend({});
    IPython.widget_manager.register_widget_model('SphereGeometryModel', SphereGeometryModel);

    var MaterialModel = Backbone.Model.extend({});
    IPython.widget_manager.register_widget_model('MaterialModel', MaterialModel);

    var MeshModel = Backbone.Model.extend({
          relations: [
      {type: Backbone.One,
        key: 'geometry',
        relatedModel:SphereGeometryModel},
      {type: Backbone.One,
        key: 'material',
        relatedModel:MaterialModel},
      ],
    });
    IPython.widget_manager.register_widget_model('MeshModel', MeshModel);
    var CameraModel = Backbone.AssociatedModel.extend({});
    IPython.widget_manager.register_widget_model('CameraModel', CameraModel);   

    var SceneModel = Backbone.AssociatedModel.extend({
      relations: [
      {type: Backbone.Many,
        key: 'meshes',
        relatedModel:MeshModel},
      ],
    });
    IPython.widget_manager.register_widget_model('SceneModel', SceneModel);

    var RendererModel = IPython.WidgetModel.extend({
        initialize: function() {
            // for each object in the hash
            //initialize the models
            // objects is a hash of every object
        }
        
      relations: [
      {type: Backbone.One,
        key: 'scene',
        relatedModel:SceneModel},
      {type: Backbone.One,
        key: 'camera',
        relatedModel:CameraModel},
      ],
    });
    IPython.widget_manager.register_widget_model('RendererModel', RendererModel);        
})


Goals

Traitlet objects in python, and some way to specify the scene graph relationship. Possibly the scene graph is represented separately from the collection of objects, and possibly the objects form a nested scenegraph

Communication with the front end over a single comm link---so the renderer is the only "widget". Changes to each of the objects is communicated through it.

Events on the javascript side

Possible ways to do it

  • have a flat set of objects and a separate scenegraph showing the relationships between the objects.

    • easy to sync changes (each object has an id; changes contain the id number)
    • fits with backbone better, sort of. We just need a heterogeneous collection of models, plus a scenegraph model.
  • nested set of objects

    • more natural to construct
    • harder to specify events---you have to traverse the hierarchy first

In [2]:


In [ ]:
ran?

In [3]:
# Import the base Widget class and the traitlets Unicode class.
from IPython.html.widgets.widget import Widget, NonDOMWidget
from IPython.utils.traitlets import Unicode, Int, Instance, Enum, List, Float

class Geometry(NonDOMWidget):
    target_name = Unicode('GeometryModel')
    default_view_name = Unicode('GeometryView')

class SphereGeometry(Geometry):
    target_name = Unicode('SphereGeometryModel')
    default_view_name = Unicode('SphereGeometryView')
    _keys = ['radius']
    radius = Int(100)

class Material(NonDOMWidget):
    target_name = Unicode('MaterialModel')
    default_view_name = Unicode('MaterialView')
    _keys = ['color']
    color = Int(0x00cc00)

class Mesh(NonDOMWidget):
    target_name = Unicode('MeshModel')
    default_view_name = Unicode('MeshView')
    _keys = ['geometry', 'material']
    geometry = Instance(Geometry)
    material = Instance(Material)

class Camera(NonDOMWidget):
    target_name = Unicode('CameraModel')
    default_view_name = Unicode('CameraView')
    _keys = ['fov', 'ratio']
    fov = Int(70)
    ratio = Float(600.0/400.0)
    
class Scene(NonDOMWidget):
    target_name = Unicode('SceneModel')
    default_view_name = Unicode('SceneView') 
    _keys = ['meshes']
    meshes = List(Instance(Mesh))

class Renderer(Widget):
    target_name = Unicode('RendererModel')
    default_view_name = Unicode('RendererView')
    _keys = ['width', 'height', 'renderer_type', 'scene', 'camera']
    width = Int(600)
    height = Int(400)
    renderer_type = Enum(['webgl', 'canvas', 'auto'], 'auto')
    scene = Instance(Scene)
    camera = Instance(Camera)

In [4]:
%%javascript
require(["notebook/js/widget"], function() {
    var RendererView = IPython.WidgetView.extend({
        render : function(){
            var width = this.model.get('width');
            var height = this.model.get('height');
            this.renderer = new THREE.WebGLRenderer();
            this.renderer.setSize( width, height);
            this.$el.empty().append( this.renderer.domElement );
            this.camera = new CameraView({model: this.model.get('camera')});
            this.camera.render();
            this.scene = new SceneView({model: this.model.get('scene')});
            this.scene.render();
            console.log('renderer', this.model, this.scene.obj);
        },
        update : function(){
            console.log('update');
            // TODO: tie into a requestAnimationFrame update (just trigger a frame if one is not already lined up)
            this.renderer.render(this.scene.obj, this.camera.obj);
            return IPython.WidgetView.prototype.update.call(this);
            // perhaps we should just listen to the nested-change event
            // and perform the appropriate modificatino ourselves?
            // this assumes that (1) we've stored the three.js hierarchy exactly as the models are set up
            // and (2) all changes to the three.js objects are exactly the same (e.g., setting attributes)
            // regardless, we should probably listen to the nested-change event to trigger a redraw
        },        
    });

    IPython.widget_manager.register_widget_view('RendererView', RendererView);
    
    var NestedView = function(options) {
        this.cid = _.uniqueId('view');
        options || (options = {});
        _.extend(this, _.pick(options, ['model', 'id']));
        this.initialize.apply(this, arguments);
        this.model.on('change', this.update, this);
        this.visible = true;
    };
    _.extend(NestedView.prototype, Backbone.Events, {
        initialize: function(){},
        render: function() {
          return this;
        },
        remove: function() {
          this.stopListening();
          return this;
        },
    });
    NestedView.extend = Backbone.View.extend;
    var CameraView = NestedView.extend({
        render: function() {
            var camera = this.obj = new THREE.PerspectiveCamera( this.model.get('fov'), this.model.get('ratio'), 1, 1000 );
            camera.position.set(0,150,400);
            return camera;
        }
    });
    
    var SceneView = NestedView.extend({
        render: function() {
            var scene = this.obj = new THREE.Scene();
            var light = new THREE.PointLight(0xffffff);
            light.position.set(100,250,100);
            scene.add(light);
            var that = this;
            this.meshviews = [];
            this.model.get('meshes').each(function(model) {
                var m = new MeshView({model: model})
                that.meshviews.push(m)
                scene.add(m.render());
            })
            this.model.get('lights').each(function(model) {
                var m = new LightView({model: model})
                that.lightviews.push(m);
                scene.add(m.render());
            })
            return scene;
        }    
    });
// TODO: most change events are very similar---just change the three.js object appropriately.
// maybe 
    
    var GeometryView = NestedView.extend({
        render: function() {
            var geometry = this.obj = new THREE.SphereGeometry(this.model.get('radius'), 32, 16);
            return geometry;
        }
        
        // update will need to generate a new mesh (to change the radius...)
    })
    
    var MaterialView = NestedView.extend({
        render: function() {
            var material = this.obj = new THREE.MeshLambertMaterial({color: this.model.get('color')});
            return material;
        }
    })
    
    var MeshView = NestedView.extend({
        render: function() {
            this.geometryview = new GeometryView({model: this.model.get('geometry')});
            this.materialview = new MaterialView({model: this.model.get('material')});
            var mesh = this.obj = new THREE.Mesh( this.geometryview.render(), this.materialview.render() );
            return mesh
        }    
    });   
});



In [5]:
from IPython.display import display
sphere = Mesh(geometry=SphereGeometry(), material=Material())
scene = Scene(meshes=[sphere])
renderer = Renderer(camera=Camera(), scene = scene)
renderer.get_state()
display(renderer)


{'width': 600, '_remove_class': [0], '_css': {}, 'scene': {'visible': True, 'meshes': [{'geometry': {'visible': True, 'radius': 100}, 'visible': True, 'material': {'color': 52224, 'visible': True}}]}, 'height': 400, 'visible': True, 'camera': {'visible': True, 'fov': 70, 'ratio': 1.5}, '_add_class': [0], 'renderer_type': 'auto'}

In [ ]: