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.
I thought more about it:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]:
In [ ]: