aframe-stereo-component
Version:
Stereoscopic component for A-Frame VR.
219 lines (146 loc) • 7.68 kB
JavaScript
module.exports = {
// Put an object into left, right or both eyes.
// If it's a video sphere, take care of correct stereo mapping for both eyes (if full dome)
// or half the sphere (if half dome)
'stereo_component' : {
schema: {
eye: { type: 'string', default: "left"},
mode: { type: 'string', default: "full"},
split: { type: 'string', default: "horizontal"},
playOnClick: { type: 'boolean', default: true },
},
init: function(){
// Flag to acknowledge if 'click' on video has been attached to canvas
// Keep in mind that canvas is the last thing initialized on a scene so have to wait for the event
// or just check in every tick if is not undefined
this.video_click_event_added = false;
this.material_is_a_video = false;
// Check if material is a video from html tag (object3D.material.map instanceof THREE.VideoTexture does not
// always work
if(this.el.getAttribute("material")!==null && 'src' in this.el.getAttribute("material") && this.el.getAttribute("material").src !== "") {
var src = this.el.getAttribute("material").src;
// If src is an object and its tagName is video...
if (typeof src === 'object' && ('tagName' in src && src.tagName === "VIDEO")) {
this.material_is_a_video = true;
}
}
var object3D = this.el.object3D.children[0];
// In A-Frame 0.2.0, objects are all groups so sphere is the first children
// Check if it's a sphere w/ video material, and if so
// Note that in A-Frame 0.2.0, sphere entities are THREE.SphereBufferGeometry, while in A-Frame 0.3.0,
// sphere entities are THREE.BufferGeometry.
var validGeometries = [THREE.SphereGeometry, THREE.BufferGeometry];
var isValidGeometry = validGeometries.some(function(geometry) {
return object3D.geometry instanceof geometry;
});
if (isValidGeometry && this.material_is_a_video) {
// if half-dome mode, rebuild geometry (with default 100, radius, 64 width segments and 64 height segments)
if (this.data.mode === "half") {
var geo_def = this.el.getAttribute("geometry");
var geometry = new THREE.SphereGeometry(geo_def.radius || 100, geo_def.segmentsWidth || 64, geo_def.segmentsHeight || 64, Math.PI / 2, Math.PI, 0, Math.PI);
}
else {
var geo_def = this.el.getAttribute("geometry");
var geometry = new THREE.SphereGeometry(geo_def.radius || 100, geo_def.segmentsWidth || 64, geo_def.segmentsHeight || 64);
}
// Panorama in front
object3D.rotation.y = Math.PI / 2;
// Calculate texture offset and repeat and modify UV's
// (cannot use in AFrame material params, since mappings are shared when pointing to the same texture,
// thus, one eye overrides the other) -> https://stackoverflow.com/questions/16976365/two-meshes-same-texture-different-offset
var axis = this.data.split === 'horizontal' ? 'y' : 'x';
// If left eye is set, and the split is horizontal, take the left half of the video texture.
// If the split is set to vertical, take the top/upper half of the video texture.
// UV texture coordinates start at the bottom left point of the texture, so y axis coordinates for left eye on vertical split
// are 0.5 - 1.0, and for the right eye are 0.0 - 0.5
var offset = this.data.eye === 'left' ? (axis === 'y' ? {x: 0, y: 0} : {x: 0, y: 0.5}) : (axis === 'y' ? {x: 0.5, y: 0} : {x: 0, y: 0});
var repeat = axis === 'y' ? {x: 0.5, y: 1} : {x: 1, y: 0.5};
var uvAttribute = geometry.attributes.uv;
for (var i = 0; i < uvAttribute.count; i++ ) {
var u = uvAttribute.getX(i)*repeat.x + offset.x;
var v = uvAttribute.getY(i)*repeat.y + offset.y;
uvAttribute.setXY(i, u, v);
}
// Needed in BufferGeometry to update UVs
uvAttribute.needsUpdate = true
this.originalGeometry = object3D.geometry;
object3D.geometry = geometry
}
else{
// No need to attach video click if not a sphere and not a video, set this to true
this.video_click_event_added = true;
}
},
remove: function(){
var object3D = this.el.object3D.children[0];
object3D.geometry.dispose();
if (this.originalGeometry) {
object3D.geometry = this.originalGeometry;
}
},
// On element update, put in the right layer, 0:both, 1:left, 2:right (spheres or not)
update: function(oldData){
var object3D = this.el.object3D.children[0];
var data = this.data;
if(data.eye === "both"){
object3D.layers.set(0);
}
else{
object3D.layers.set(data.eye === 'left' ? 1:2);
}
},
tick: function(time){
// If this value is false, it means that (a) this is a video on a sphere [see init method]
// and (b) of course, tick is not added
if(!this.video_click_event_added && this.data.playOnClick){
if(typeof(this.el.sceneEl.canvas) !== 'undefined'){
// Get video DOM
this.videoEl = this.el.object3D.children[0].material.map.image;
// On canvas click, play video element. Use self to not lose track of object into event handler
var self = this;
this.el.sceneEl.canvas.onclick = function () {
self.videoEl.play();
};
// Signal that click event is added
this.video_click_event_added = true;
}
}
}
},
// Sets the 'default' eye viewed by camera in non-VR mode
'stereocam_component':{
schema: {
eye: { type: 'string', default: "left"}
},
// Cam is not attached on init, so use a flag to do this once at 'tick'
// Use update every tick if flagged as 'not changed yet'
init: function(){
// Flag to register if cam layer has already changed
this.layer_changed = false;
},
tick: function(time){
var originalData = this.data;
// If layer never changed
if(!this.layer_changed){
// because stereocam component should be attached to an a-camera element
// need to get down to the root PerspectiveCamera before addressing layers
// Gather the children of this a-camera and identify types
var childrenTypes = [];
this.el.object3D.children.forEach( function (item, index, array) {
childrenTypes[index] = item.type;
});
// Retrieve the PerspectiveCamera
var rootIndex = childrenTypes.indexOf("PerspectiveCamera");
var rootCam = this.el.object3D.children[rootIndex];
if(originalData.eye === "both"){
rootCam.layers.enable( 1 );
rootCam.layers.enable( 2 );
}
else{
rootCam.layers.enable(originalData.eye === 'left' ? 1:2);
}
this.layer_changed = true;
}
}
}
};