elation-engine
Version:
WebGL/WebVR engine written in Javascript
1,266 lines (1,165 loc) • 80.7 kB
JavaScript
if (typeof THREE == 'undefined') {
THREE = {};
}
elation.require([
"engine.external.three.three",
"engine.external.three.three-objects",
"engine.external.three.three-controls",
"engine.external.three.three-postprocessing",
"engine.external.three.three-shaders",
"engine.external.three.three-extras",
"engine.external.three.CSS3DRenderer",
"engine.external.three.CubemapToEquirectangular",
"engine.external.holoplay",
"engine.external.webxr-polyfill",
"engine.materials",
"engine.geometries",
//"ui.select",
//"ui.slider",
'elements.base',
], function() {
elation.requireCSS('engine.systems.render');
elation.extend("engine.systems.render", function(args) {
elation.implement(this, elation.engine.systems.system);
this.views = {};
this.forcerefresh = false;
this.view_init = function(viewname, viewargs) {
let newview = elation.elements.create('engine-systems-render-view', viewargs);
return this.view_add(viewname, newview);
}
this.view_add = function(viewname, view) {
this.views[viewname] = view;
elation.events.fire({type: 'render_view_add', element: this, data: this.views[viewname]});
return this.views[viewname];
}
this.view_remove = function(viewname) {
if (viewname in this.views) {
this.views[viewname].destroy();
delete this.views[viewname];
}
}
this.system_attach = function(ev) {
console.log('INIT: render');
let webglmode = 'webgl';
let rendererargs = {
antialias: true,
logarithmicDepthBuffer: false,
alpha: true,
preserveDrawingBuffer: true,
enableWebXR: false,
stencil: false,
powerPreference: 'high-performance',
//precision: 'lowp',
};
if (webglmode == 'webgl2') {
rendererargs.canvas = document.createElement( 'canvas' );
rendererargs.context = rendererargs.canvas.getContext( 'webgl2', { antialias: true } );
}
this.renderer = new THREE.WebGLRenderer(rendererargs);
this.cssrenderer = new THREE.CSS3DRenderer();
/*
this.holorenderer = new HoloPlay.Renderer(rendererargs);
this.holorenderer.quiltResolution = 3360;
this.holorenderer.tileCount.set(8, 6);
document.body.appendChild(this.holorenderer.domElement);
*/
//this.renderer.autoClear = true;
this.renderer.setClearColor(0x000000, 1);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.xr.enabled = false;
//this.renderer.xr.manageCameraPosition = true;
this.lastframetime = performance.now();
this.renderer.setAnimationLoop((time, frame) => {
this.render(time - this.lastframetime, frame);
this.lastframetime = time;
});
//this.renderer.setAnimationLoop((ev) => { this.render(); });
//this.renderer.gammaInput = true;
//this.renderer.gammaOutput = false;
//this.renderer.outputEncoding = THREE.LinearEncoding;
//this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.gammaFactor = 1.3;
/*
this.renderer.toneMapping = THREE.CineonToneMapping;
this.renderer.toneMappingExposure = 1;
this.renderer.toneMappingWhitePoint = 1;
*/
this.renderer.debug.checkShaderErrors = false;
this.lastframetime = 0;
elation.events.add(this.engine.systems.world, 'world_change,world_thing_add,world_thing_remove,world_thing_change', this);
// FIXME - globally-bound events are dirty, things should fire events when their properties change
elation.events.add(null, 'physics_update,thing_drag_move,thing_rotate_move,engine_texture_load', elation.bind(this, this.setdirty));
// Hide the canvas from accessibility API
this.renderer.domElement.setAttribute('aria-hidden', true);
}
this.setclearcolor = function(color, opacity) {
if (typeof color == 'undefined') color = 0xffffff;
if (typeof opacity == 'undefined') opacity = 1;
this.renderer.setClearColor(color, opacity);
}
this.setdirty = function() {
this.dirty = true;
}
this.engine_stop = function(ev) {
console.log('SHUTDOWN: render');
for (var k in this.views) {
this.views[k].destroy();
};
}
this.engine_frame = function(ev) {
// Disabled rendering on engine_frame, use Three's setAnimationLoop instead
//this.lastframetime += ev.data.delta;
//this.render();
}
this.render = function(time, frame) {
for (var k in this.views) {
this.views[k].updatePickingObject();
if (this.views[k].stats) {
this.views[k].stats.update();
}
}
this.engine.advance();
if (this.forcerefresh || this.dirty) {
//console.log('FRAME: render');
this.dirty = false;
//this.renderer.clear();
for (var k in this.views) {
this.views[k].render(time, frame);
}
//this.lastframetime = 0;
}
}
this.textureSampleMipmapLevel = (function() {
let scene = new THREE.Scene();
let plane = new THREE.PlaneGeometry(2, 2);
let material = new THREE.MeshBasicMaterial({color: 0xffffff});
let mesh = new THREE.Mesh(plane, material);
mesh.position.set(0,0,-1);
scene.add(mesh);
let camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 1000);
scene.add(camera);
let rendertarget = new THREE.WebGLRenderTarget(1, 1);
let oldviewport = new THREE.Vector4();
let viewport = new THREE.Vector4(0, 0, 1, 1);
return function(texture, level=0) {
material.map = texture;
material.map.needsUpdate = true;
let size = Math.pow(2, level);
rendertarget.setSize(size, size);
let renderer = this.renderer;
let pixeldata = new Uint8Array(4 * size * size);
let oldrendertarget = renderer.getRenderTarget();
let oldencoding = texture.encoding;
texture.encoding = THREE.LinearEncoding;
renderer.getViewport(oldviewport);
renderer.setViewport(viewport);
renderer.setRenderTarget(rendertarget);
let isXR = renderer.xr.enabled;
renderer.xr.enabled = false;
renderer.render(scene, camera);
renderer.readRenderTargetPixels(rendertarget, 0, 0, size, size, pixeldata);
renderer.setRenderTarget(oldrendertarget);
renderer.setViewport(oldviewport);
renderer.xr.enabled = isXR;
texture.encoding = oldencoding;
return pixeldata;
}
})();
this.textureHasAlpha = (function() {
let scene = new THREE.Scene();
let plane = new THREE.PlaneGeometry(2, 2);
//let material = new THREE.MeshBasicMaterial({color: 0xff0000});
let material = new THREE.ShaderMaterial({
vertexShader: `
uniform sampler2D map;
varying vec2 vUv;
void main() {
mat4 modelViewProjectionMatrix = projectionMatrix * modelViewMatrix;
vec3 transformed = vec3( position );
// Transform our new vertex position into clip space
vec4 pos = modelViewProjectionMatrix * vec4(transformed, 1.0);
gl_Position = pos;
vUv = uv;
}
`,
fragmentShader: `
uniform sampler2D map;
varying vec2 vUv;
void main(){
vec4 sampledDiffuseColor = texture2D( map, vUv );
gl_FragColor = vec4(sampledDiffuseColor.rgb, floor(sampledDiffuseColor.a));
//gl_FragColor = vec4(sampledDiffuseColor.rgb, 0.0);
}
`,
uniforms: {
map: { type: 't' },
},
});
let mesh = new THREE.Mesh(plane, material);
mesh.position.set(0,0,-1);
scene.add(mesh);
let camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 1000);
scene.add(camera);
let rendertarget = new THREE.WebGLRenderTarget(1, 1);
let oldviewport = new THREE.Vector4();
let viewport = new THREE.Vector4(0, 0, 1, 1);
return function(texture) {
material.uniforms.map.value = texture;
texture.needsUpdate = true;
let renderer = this.renderer;
let pixeldata = new Uint8Array(4);
let oldrendertarget = renderer.getRenderTarget();
renderer.getViewport(oldviewport);
renderer.setViewport(viewport);
renderer.setRenderTarget(rendertarget);
renderer.render(scene, camera);
renderer.readRenderTargetPixels(rendertarget, 0, 0, 1, 1, pixeldata);
renderer.setRenderTarget(oldrendertarget);
renderer.setViewport(oldviewport);
return pixeldata[3] < 255;
}
})()
this.world_thing_add = function(ev) {
this.setdirty();
}
this.world_thing_remove = function(ev) {
this.setdirty();
}
this.world_thing_change = function(ev) {
this.setdirty();
}
this.world_change = function(ev) { this.setdirty();
}
});
elation.elements.define("engine.systems.render.view", class extends elation.elements.base {
//elation.implement(this, elation.engine.systems.system);
init() {
super.init();
this.effects = {};
this.defineAttributes({
picking: { type: 'boolean', default: false },
useWebXRPolyfill: { type: 'boolean', default: true },
size: { type: 'object', default: [0, 0] },
showstats: { type: 'boolean', default: false },
fullsize: { type: 'boolean', default: false },
rendermode: { type: 'string', default: 'default' },
resolution: { type: 'object' },
enabled: { type: 'boolean', default: true },
crosshair: { type: 'boolean', default: false },
engine: { type: 'string', default: 'default' },
xrsession: { type: 'object' },
enablepostprocessing: { type: 'boolean', default: true },
});
//elation.html.addclass(this, "engine_view");
//this.picking = this.args.picking || false;
//this.useWebXRPolyfill = elation.utils.any(this.args.useWebXRPolyfill, true);
//this.size = [0, 0];
this.size_old = [0, 0];
this.scale = 100;// * devicePixelRatio;
//this.showstats = this.args.showstats || false;
this.fullscreen = false;
this.renderpasses = {};
this.aspectscale = 1;
this.renderinfo = {render: {}, memory: {}};
// Used by various render pass shaders
this.sizevec = new THREE.Vector2();
this.sizevecinverse = new THREE.Vector2();
//this.rendermode = this.args.rendermode || 'default';
/*
if (this.fullsize) {
elation.html.addclass(this, "engine_view_fullsize");
}
if (this.resolution) {
elation.html.addclass(this, "engine_view_fixedsize");
}
if (this.crosshair) {
elation.html.create({tag: 'div', classname: 'engine_view_crosshair', append: this});
}
*/
}
create() {
super.create();
if (!this.engine) {
console.log("ERROR: couldn't create view, missing engine parameter");
} else if (typeof elation.engine.instances[this.engine] == 'undefined') {
console.log("ERROR: couldn't create view, engine '" + this.engine + "' doesn't exist");
} else {
this.engine = elation.engine.instances[this.engine];
//this.createView();
}
this.rendersystem = this.engine.systems.render;
this.tabIndex = 1;
this.canvas = this.rendersystem.renderer.domElement;
if (!this.fullsize) {
this.style.width = '100%';
this.style.height = '100%';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
}
elation.events.add(window, "resize", this);
elation.events.add(window, "orientationchange", ev => this.resize(true));
elation.events.add(document.body, "mouseenter,mouseleave", this);
elation.events.add(this.canvas, "mouseover,mousedown,mousemove,mouseup,click", this);
elation.events.add(this.canvas, "mousewheel,touchstart,touchmove,touchend", this);
elation.events.add(document, "pointerlockchange,mozpointerlockchange", elation.bind(this, this.pointerlockchange));
elation.events.add(this, "dragover,drag,dragenter,dragleave,dragstart,dragend,drop", elation.bind(this, this.proxyEvent));
if (this.rendersystem.renderer.domElement && !this.rendersystem.renderer.domElement.parentNode) {
this.appendChild(this.rendersystem.renderer.domElement);
// Handle context loss and restore
elation.events.add(this.rendersystem.renderer.domElement, 'webglcontextlost', elation.bind(this, function(ev) {
console.warn('WebGL context lost');
ev.preventDefault();
//this.engine.stop();
}));
elation.events.add(this.rendersystem.renderer.domElement, 'webglcontextrestored', elation.bind(this, function(ev) {
console.log('context restored');
ev.preventDefault();
//this.engine.start();
this.create();
this.rendersystem.setdirty();
}));
}
if (this.rendersystem.cssrenderer && !this.rendersystem.cssrenderer.domElement.parentNode) {
this.appendChild(this.rendersystem.cssrenderer.domElement);
elation.html.addclass(this.rendersystem.cssrenderer.domElement, 'engine_systems_render_css3d');
}
if (this.xrsession) {
this.setXRSession(this.xrsession);
}
this.rendersystem.view_add(this.name, this);
if (!this.actualcamera) {
var cam = new THREE.PerspectiveCamera(75, 4/3, 1e-2, 1e4);
this.actualcamera = cam;
}
this.setcamera(this.actualcamera);
if (this.pickingdebug) {
this.setscene(this.engine.systems.world.scene['colliders']);
} else {
this.setscene(this.engine.systems.world.scene['world-3d']);
}
if (this.engine.systems.world.scene['sky']) {
this.setskyscene(this.engine.systems.world.scene['sky']);
}
//console.log(this.engine.systems.world.scene['world-3d']);
// Depth shader, used for SSAO, god rays, etc
if (false && !this.depthTarget) {
var depthShader = THREE.ShaderLib[ "depth" ];
var depthUniforms = THREE.UniformsUtils.clone( depthShader.uniforms );
this.depthMaterial = new THREE.ShaderMaterial( { fragmentShader: depthShader.fragmentShader, vertexShader: depthShader.vertexShader, uniforms: depthUniforms } );
this.depthMaterial.blending = THREE.NoBlending;
this.depthTarget = new THREE.WebGLRenderTarget( this.size[0], this.size[1], {
minFilter: THREE.NearestFilter,
magFilter: THREE.NearestFilter,
});
}
//this.composer = this.createRenderPath([this.rendermode]);
this.rendertarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
minFilter: THREE.LinearFilter,
magFilter: THREE.LinearFilter,
format: THREE.RGBAFormat,
//type: THREE.FloatType,
//type: THREE.UnsignedShortType,
stencilBuffer: true,
depthBuffer: true,
});
this.rendertarget.texture.encoding = THREE.sRGBEncoding;
this.rendertarget.depthTexture = new THREE.DepthTexture();
this.rendertarget.depthTexture.type = THREE.UnsignedInt248Type;
this.rendertarget.depthTexture.format = THREE.DepthStencilFormat;
//this.composer = this.createRenderPath(['clear', /*'portals', 'masktest',*/ this.rendermode, 'fxaa'/*, 'msaa'*/, 'bloom', 'maskclear', 'recording'], this.rendertarget);
if (this.enablepostprocessing) {
this.composer = this.createRenderPath(['clear', this.rendermode,/* 'tonemapping',*/ 'unrealbloom', 'fxaa', 'gamma'], this.rendertarget);
//this.composer = this.createRenderPath(['clear', this.rendermode, 'fxaa'/*, 'msaa'*/, 'bloom', 'maskclear'], this.rendertarget);
//this.effects['msaa'].enabled = false;
//this.composer = this.createRenderPath([this.rendermode, 'ssao', 'recording']);
if (this.showstats) {
// FIXME - not smart!
elation.events.add(this.composer.passes[3], 'render', elation.bind(this, this.updateRenderStats));
}
}
this.getsize();
if (this.showstats) {
this.stats = new Stats();
this.stats.domElement.style.position = 'absolute';
this.stats.domElement.style.top = '0px';
this.stats.domElement.style.right = '0px';
this.appendChild(this.stats.domElement);
this.renderstats = new THREEx.RendererStats()
this.renderstats.domElement.style.position = 'absolute'
this.renderstats.domElement.style.right = '0px'
this.renderstats.domElement.style.top = '50px'
this.appendChild( this.renderstats.domElement )
}
this.glcontext = this.rendersystem.renderer.getContext();
if (this.picking) {
this.mousepos = [0, 0, document.body.scrollTop];
this.lastmousepos = [-1, -1, -1];
this.initPicking();
this.pickingdebug = false;
this.engine.systems.controls.addCommands('view', {
'picking_debug': elation.bind(this, function(ev) {
if (ev.value == 1) {
this.pickingdebug = !this.pickingdebug;
this.rendersystem.dirty = true;
if (this.pickingdebug) {
this.engine.systems.world.enableDebug();
} else {
this.engine.systems.world.disableDebug();
}
}
}),
'picking_select': elation.bind(this, function(ev) {
if (ev.value == 1) {
this.click({clientX: this.mousepos[0], clientY: this.mousepos[1]});
}
})
});
//this.engine.systems.controls.addBindings('view', {'keyboard_f7': 'picking_debug'});
this.engine.systems.controls.addBindings('view', {'gamepad_any_button_0': 'picking_select'});
this.engine.systems.controls.activateContext('view');
}
}
destroy() {
// TODO - deeallocate resources
}
createRenderPath(passes, target) {
// this.createRenderPath(['picking', 'oculus_deform'], depthTarget)
// this.createRenderPath(['depth', 'oculus_deform'], pickingTarget)
// this.createRenderPath(['sky', 'default', 'FXAA', 'oculus_deform', 'oculus_colorshift'])
var composer = new THREE.EffectComposer(this.rendersystem.renderer, target);
var renderToScreen = false;
for (var i = 0; i < passes.length; i++) {
var pass = this.createRenderPass(passes[i]);
//console.log('NEW PASS:', i, target, passes[i], pass);
if (pass) {
//if (i == 0) pass.clear = true;
composer.addPass(pass);
renderToScreen = renderToScreen || pass.renderToScreen;
// Only store pass data for the main path
if (!target) {
this.renderpasses[passes[i]] = pass;
}
}
}
//if (!target && !renderToScreen) {
var pass = this.createRenderPass('screenout');
composer.addPass(pass);
//}
return composer;
}
createRenderPass(name, args) {
var pass = false;
switch (name) {
case 'default':
pass = new THREE.RenderPass(this.scene, this.actualcamera, null, null, 1);
pass.clear = false;
break;
case 'normalmap':
pass = new THREE.RenderPass(this.scene, this.actualcamera, new THREE.MeshNormalMaterial(), null, 1);
pass.clear = false;
break;
case 'depth':
pass = new THREE.RenderPass(this.scene, this.actualcamera, new THREE.MeshDepthMaterial(), null, 1);
pass.clear = false;
break;
case 'portals':
pass = new THREE.PortalRenderPass(this.actualcamera);
pass.clear = false;
this.portalpass = pass;
break;
case 'clear':
var pass = new THREE.ClearPass();
break;
case 'oculus':
pass = new THREE.OculusRenderPass(this.scene, this.actualcamera, null, null, 0);
pass.setOculusParameters({
HMD: {
hResolution: window.innerWidth,
vResolution: window.innerHeight,
hScreenSize: 0.14976,
vScreenSize: 0.0936,
interpupillaryDistance: 0.064,
lensSeparationDistance: 0.064,
eyeToScreenDistance: 0.041,
distortionK : [1.0, 0.0, 0.0, 0.0],
chromaAbParameter: [ 1, 0, 1, 0.0]
}
});
break;
case '3dtvsbs':
pass = new THREE.OculusRenderPass(this.scene, this.actualcamera, null, null, 0);
pass.setOculusParameters({
HMD: {
hResolution: window.innerWidth,
vResolution: window.innerHeight,
hScreenSize: 0.14976,
vScreenSize: 0.0936,
interpupillaryDistance: 0.064,
lensSeparationDistance: 0.064,
eyeToScreenDistance: 0.041,
distortionK : [1.0, 0.0, 0.0, 0.0],
chromaAbParameter: [ 1, 0, 1, 0.0]
}
});
break;
case 'sky':
pass = new THREE.RenderPass(this.skyscene, this.skycamera, null, null, 0);
pass.clear = false;
break;
case 'film':
pass = new THREE.FilmPass( 0.35, .75, 2048, false );
break;
case 'recording':
pass = new THREEcapRenderPass('/scripts/engine/external/threecap/');
this.recorder = pass;
break;
case 'sepia':
pass = new THREE.ShaderPass( THREE.SepiaShader );
break;
case 'bleach':
pass = new THREE.ShaderPass( THREE.BleachBypassShader );
break;
case 'copy':
pass = new THREE.ShaderPass( THREE.CopyShader );
break;
case 'screenout':
pass = new THREE.ShaderPass( THREE.CopyShader );
pass.renderToScreen = true;
break;
case 'bloom':
pass = new THREE.BloomPass(0.4, 25, 5);
break;
case 'unrealbloom':
pass = new THREE.UnrealBloomPass(this.size, 0, 0, 0);
pass.renderTargetsHorizontal.forEach(element => {
element.texture.type = THREE.FloatType;
});
pass.renderTargetsVertical.forEach(element => {
element.texture.type = THREE.FloatType;
});
break;
case 'fxaa':
pass = new THREE.ShaderPass( THREE.FXAAShader );
pass.uniforms[ 'resolution' ].value = this.sizevecinverse;
break;
case 'msaa':
pass = new THREE.ManualMSAARenderPass(this.scene, this.actualcamera);
pass.unbiased = true;
pass.sampleLevel = 1;
break;
case 'masktest':
this.maskscene = new THREE.Scene();
var maskobj = new THREE.Mesh(new THREE.SphereGeometry(1000));
maskobj.scale.y = -1;
maskobj.position.set(0,0,0);
window.maskobj = maskobj;
this.maskscene.add(maskobj);
pass = new THREE.MaskPass(this.maskscene, this.actualcamera);
pass.clear = false;
break;
case 'maskclear':
pass = new THREE.ClearMaskPass();
break;
case 'ssao':
pass = new THREE.SSAOPass( this.scene, this.actualcamera, this.size[0], this.size[1] );
pass.kernelRadius = 16;
//pass.clear = true;
break;
case 'gamma':
pass = new THREE.ShaderPass( THREE.GammaCorrectionShader );
break;
case 'tonemapping':
pass = new THREE.AdaptiveToneMappingPass(true, 256);
break;
}
if (pass) this.effects[name] = pass;
return pass;
}
setRenderMode(mode) {
// Supported values: 'default', 'oculus'
var lastpass = this.renderpasses[this.rendermode];
var pass = this.renderpasses[mode];
if (!pass) {
pass = this.createRenderPass(mode);
this.renderpasses[mode] = pass;
}
var passidx = this.composer.passes.indexOf(lastpass);
console.log('toggle render mode: ' + this.rendermode + ' => ' + mode, passidx, lastpass, pass, this.renderpasses);
this.composer.passes[passidx] = pass;
if (this.pickingcomposer) this.pickingcomposer.passes[passidx] = pass;
pass.camera = this.actualcamera;
elation.html.removeclass(this, "engine_view_rendermode_" + this.rendermode);
this.rendermode = mode;
elation.html.addclass(this, "engine_view_rendermode_" + this.rendermode);
this.rendersystem.setdirty();
}
isFullscreen() {
var fsel = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
if (fsel) {
return true;
}
return false;
}
toggleFullscreen(fullscreen) {
if (typeof fullscreen == 'undefined') {
fullscreen = !this.isFullscreen();
} else if (typeof fullscreen.data != 'undefined') {
fullscreen = fullscreen.data;
}
if (fullscreen) {
//var c = this.container;
var c = document.documentElement;
c.requestFullscreen = c.requestFullscreen || c.webkitRequestFullscreen || c.mozRequestFullScreen;
if (typeof c.requestFullscreen == 'function') {
c.requestFullscreen();
}
} else {
var fsel = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
if (fsel) {
document.exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen || document.mozExitFullScreen;
document.exitFullscreen();
}
}
this.fullscreen = fullscreen;
}
startXR(mode='immersive-vr') {
}
async setXRSession(session) {
this.xrsession = session;
await this.rendersystem.renderer.xr.setSession(session);
this.rendersystem.renderer.xr.enabled = true;
this.rendersystem.renderer.outputEncoding = THREE.LinearEncoding;
this.xrlayer = this.getXRBaseLayer(session);
if (false && !this.xrscene) {
// Set up a scene with an ortho camera to clone our XR framebuffer to, for display on the main screen
this.xrscene = new THREE.Scene();
this.xrscene.background = new THREE.Color(0x000000);
// Set up texture to copy the framebuffer into
let w = this.xrlayer.framebufferWidth,
h = this.xrlayer.framebufferHeight;
let data = new Uint8Array( w * h * 3 );
this.xrscenetexture = new THREE.DataTexture( data, w, h, THREE.RGBFormat );
this.xrscenetexture.minFilter = THREE.NearestFilter;
this.xrscenetexture.magFilter = THREE.NearestFilter;
// Set up the plane to render only one eye, centered in the middle of the screen
this.xrsceneplane = new THREE.Mesh(new THREE.PlaneGeometry(w, h), new THREE.MeshBasicMaterial({map: this.xrscenetexture, side: THREE.DoubleSide, color: 0xffffff}));
this.xrsceneplane.position.set(w / 4, 0, -10);
//this.xrsceneplane.rotation.set(Math.PI/4, 0, 0);
this.xrscene.add(this.xrsceneplane);
this.xrscenecam = new THREE.OrthographicCamera(-w / 4, w, h, -h / 2, -1000, 1000);
//this.xrscenecam = new THREE.PerspectiveCamera();
this.xrscene.add(this.xrscenecam);
}
}
handleXRFrame(session, frame) {
if (session) {
this.xrsession = session;
if (this.activething && this.activething.updateXR) {
this.activething.updateXR(frame);
}
let xrReferenceSpace = this.engine.systems.render.renderer.xr.getReferenceSpace();
elation.events.fire({ element: this.engine, type: 'xrframe', data: { frame, session, xrReferenceSpace } });
}
}
stopXR() {
if (this.xrsession) {
this.xrsession.end();
}
}
handleXRend(ev) {
this.rendersystem.renderer.xr.setSession(null);
this.rendersystem.renderer.xr.enabled = false;
this.enabled = false;
elation.html.removeclass(this, 'webxr_session_active');
this.xrsession = false;
this.rendersystem.renderer.outputEncoding = THREE.sRGBEncoding;
setTimeout(() => {
elation.events.fire({type: 'resize', element: window, data: true });
}, 100);
}
getXRBaseLayer(session) {
if (session.renderState.layers && session.renderState.layers.length > 0) {
return session.renderState.layers[session.renderState.layers.length - 1];
}
return session.renderState.baseLayer;
}
updateCameras() {
// Closure scratch variables
var _position = new THREE.Vector3(),
_quaternion = new THREE.Quaternion(),
_scale = new THREE.Vector3();
// Make sure the parent's matrixWorld is up to date
if (this.camera.parent) {
this.camera.parent.updateMatrix(true);
this.camera.parent.updateMatrixWorld(true);
}
if (this.actualcamera) {
// Copy this.camera's position/orientation/scale/parent to our actual camera
if (this.actualcamera.parent && this.actualcamera.parent != this.camera.parent) {
this.actualcamera.parent.remove(this.actualcamera);
}
if (this.camera.parent && this.actualcamera.parent != this.camera.parent) {
this.camera.parent.add(this.actualcamera);
}
this.actualcamera.position.copy(this.camera.position);
this.actualcamera.scale.copy(this.camera.scale);
this.actualcamera.rotation.copy(this.camera.rotation);
this.actualcamera.quaternion.copy(this.camera.quaternion);
if (this.actualcamera.fov != this.camera.fov ||
this.actualcamera.near != this.camera.near ||
this.actualcamera.far != this.camera.far ||
this.actualcamera.aspect != this.camera.aspect) {
this.actualcamera.fov = this.camera.fov;
this.actualcamera.near = this.camera.near || 0.001;
this.actualcamera.far = this.camera.far || 10000;
this.actualcamera.aspect = this.camera.aspect;
this.actualcamera.updateProjectionMatrix();
}
this.actualcamera.layers.mask = this.camera.layers.mask;
}
if (this.skycamera) {
// Sky camera needs to use our camera's world rotation, and nothing else
this.camera.matrixWorld.decompose( _position, _quaternion, _scale );
this.skycamera.quaternion.copy(_quaternion);
if (this.skycamera.fov != this.camera.fov || this.skycamera.aspect != this.camera.aspect) {
this.skycamera.fov = this.camera.fov;
this.skycamera.aspect = this.camera.aspect;
this.skycamera.updateProjectionMatrix();
}
}
}
renderloop() {
// Disable custom element renderloop
}
render(delta, xrframe) {
if (this.enabled && this.scene && this.camera) {
if (xrframe && this.xrsession) {
this.handleXRFrame(this.xrsession, xrframe);
}
if (this.size[0] != this.size_old[0] || this.size[1] != this.size_old[1] || this.scale != this.scale_old) {
this.setrendersize(this.size[0], this.size[1]);
}
this.updateCameras();
//this.setcameranearfar();
elation.events.fire({type: 'render_view_prerender', element: this});
if (this.picking && this.pickingactive) {
//if (this.pickingdebug || this.picknum++ % 3 == 0 || delta > 0.05) {
this.updatePickingTarget();
//}
}
//this.scene.overrideMaterial = this.depthMaterial;
//this.rendersystem.renderer.render(this.scene, this.actualcamera, this.depthTarget, true);
//this.scene.overrideMaterial = null;
//this.rendersystem.renderer.render(this.scene, this.actualcamera);
let colliderscene = this.engine.systems.world.scene['colliders'],
worldscene = this.engine.systems.world.scene['world-3d'];
if (this.pickingdebug) {
if (colliderscene.parent !== worldscene) {
worldscene.add(colliderscene);
}
} else if (colliderscene.parent === worldscene) {
worldscene.remove(colliderscene);
}
if (this.enablepostprocessing) {
this.effects[this.rendermode].camera = this.actualcamera;
this.composer.render(delta);
} else {
if (this.xrsession) {
//this.rendersystem.renderer.xr.enabled = true;
//this.rendersystem.renderer.xr.setSession(this.xrsession);
let layer = this.getXRBaseLayer(this.xrsession);
if (layer) {
this.xrlayer = layer;
let renderer = this.rendersystem.renderer;
renderer.clear();
renderer.render(this.scene, this.camera);
if (false && document.visibilityState == 'visible') {
if (true) {
//console.log('try to clone framebuffer onto screen', this.xrscene, this.xrscenecam, this.xrsceneplane.material.map);
/*
if (!this.xrsceneplane.material.map) {
console.log('update framebuffer texture');
this.xrsceneplane.material.map = new THREE.Texture(layer.framebuffer);
this.xrsceneplane.material.needsUpdate = true;
}
*/
renderer.xr.enabled = false;
renderer.copyFramebufferToTexture( V(layer.framebufferWidth / 2, 0, 0), this.xrscenetexture );
//renderer.setFramebuffer(null);
renderer.state.bindXRFramebuffer(null);
renderer.setRenderTarget( renderer.getRenderTarget() );
let oldOutputEncoding = renderer.outputEncoding;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.setViewport(0, 0, layer.framebufferWidth, layer.framebufferHeight);
renderer.render(this.xrscene, this.xrscenecam);
renderer.xr.enabled = true;
renderer.outputEncoding = oldOutputEncoding;
} else {
// FIXME - cloning the framebuffer to the main canvas output isn't working right as implemented, so in this codepath we just double-render
//this.rendersystem.renderer.setFramebuffer(null);
//renderer.state.bindXRFramebuffer(null);
//renderer.setRenderTarget( renderer.getRenderTarget() );
this.rendersystem.renderer.render(this.scene, this.camera);
//this.rendersystem.renderer.setFramebuffer(layer.framebuffer);
renderer.state.bindXRFramebuffer(layer.framebuffer);
renderer.setRenderTarget( renderer.getRenderTarget() );
}
}
} else {
console.log('no XR layer found', this.xrsession.renderState);
}
} else {
this.rendersystem.renderer.xr.enabled = false;
//this.rendersystem.renderer.xr.setSession(null);
//this.rendersystem.renderer.setFramebuffer(null);
//this.rendersystem.renderer.state.bindXRFramebuffer(null);
//this.rendersystem.renderer.setRenderTarget( renderer.getRenderTarget() );
this.rendersystem.renderer.render(this.scene, this.camera);
//this.holocamera.position.setFromMatrixPosition(this.camera.matrixWorld);
//this.holocamera.quaternion.setFromRotationMatrix(this.camera.matrixWorld);
//this.holocamera.position.set(0, 10, 10).applyMatrix4(this.camera.matrixWorld);
//this.holocamera.target.set(0, 0, 0).applyMatrix4(this.camera.matrixWorld);
//this.rendersystem.holorenderer.render(this.scene, this.holocamera);
}
}
if (this.rendersystem.cssrenderer && !this.xrsession) {
this.rendersystem.cssrenderer.render(this.scene, this.actualcamera);
}
}
/*
if (this.stats) {
this.stats.update();
}
*/
elation.events.fire({type: 'render_view_postrender', element: this});
this.size_old[0] = this.size[0];
this.size_old[1] = this.size[1];
this.scale_old = this.scale;
//this.camera.rotation.y += Math.PI/32 * delta;
}
updateRenderStats() {
this.renderstats.update(this.rendersystem.renderer);
var renderinfo = this.rendersystem.renderer.info;
elation.utils.merge(renderinfo.render, this.renderinfo.render);
elation.utils.merge(renderinfo.memory, this.renderinfo.memory);
//this.renderinfo.render.faces = renderinfo.render.faces;
}
toggleStats() {
if (this.showstats) {
if (this.renderstats) {
this.removeChild(this.renderstats.domElement)
}
if (this.stats) {
this.removeChild(this.stats.domElement);
}
this.showstats = false;
} else {
if (this.renderstats) {
this.appendChild(this.renderstats.domElement)
}
if (this.stats) {
this.appendChild(this.stats.domElement);
}
this.showstats = true;
}
}
setactivething(thing) {
if (thing.camera) {
this.setcamera(thing.camera);
}
this.activething = thing;
}
setcamera(camera) {
if (camera instanceof elation.component.base && camera.type == 'camera') {
camera = camera.objects['3d'];
}
this.camera = camera;
/*
this.holocamera = new HoloPlay.Camera();
this.holocamera.fov = 90;
console.log('my holocamera', this.holocamera, this.holocamera.fov);
//camera.add(this.holocamera);
this.holocamera.position.set(0,0,0);
this.holocamera.target.set(0,0,1);
this.holocamera.quaternion.set(0,0,0,1);
*/
this.setscene(this.getscene(camera));
this.updateCameras();
if (this.size[0] > 0 && this.size[1] > 0) {
this.setrendersize(this.size[0], this.size[1]);
}
/*
if (this.composer) {
this.composer.passes[0].camera = this.camera;
}
if (this.pickingcomposer) {
this.pickingcomposer.passes[0].camera = this.camera;
}
*/
}
setscene(scene) {
var oldscene = this.scene;
this.scene = scene;
if (this.composer) {
for (var i = 0; i < this.composer.passes.length; i++) {
var pass = this.composer.passes[i];
if (pass.scene && pass.scene === oldscene) {
pass.scene = this.scene;
}
}
}
this.rendersystem.setdirty();
}
setcameranearfar(near, far) {
/*
if (!this.camdebug) {
this.camdebug = elation.ui.window('camdebug', elation.html.create({append: document.body}), {title: 'Camera Debug'});
}
*/
if (!near || !far) {
near = Infinity, far = 0;
var nearradius = 0, farradius = 0;
var campos = new THREE.Vector3().setFromMatrixPosition(this.camera.matrixWorld);
var objpos = new THREE.Vector3();
var frustum = new THREE.Frustum();
var frustmat = new THREE.Matrix4().makePerspective( this.camera.fov, this.camera.aspect, 0.00001, 9e99).multiply(this.camera.matrixWorldInverse);
//frustum.setFromMatrix( new THREE.Matrix4().multiplyMatrices( this.camera.projectionMatrix, this.camera.matrixWorldInverse ) );
frustum.setFromProjectionMatrix(frustmat);
var within = [], nearnode = null, farnode = null;
this.scene.traverse(elation.bind(this, function(node) {
objpos.setFromMatrixPosition(node.matrixWorld);
if (!node.isBoundingSphere && node.geometry && node.geometry.boundingSphere && frustum.intersectsSphere({center: objpos, radius: node.geometry.boundingSphere.radius})) {
var distsq = objpos.distanceToSquared(campos);
var rsq = node.geometry.boundingSphere.radius * node.geometry.boundingSphere.radius;
var tdist = distsq - rsq;
if (tdist <= 0) {
within.push(node);
} else {
near = distsq;
nearnode = node;
}
if (distsq + rsq > far) {
far = distsq + rsq;
farradius = node.geometry.boundingSphere.radius;
farnode = node;
}
}
}));
if (nearnode) {
within.push(nearnode);
}
if (within.length > 0) {
var vpos = new THREE.Vector3();
for (var n = 0; n < within.length; n++) {
if (within[n].geometry instanceof THREE.BufferGeometry) {
let positions = within[n].geometry.attributes.position;
for (var i = 0; i < positions.count; i++) {
vpos.set(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]);
within[n].localToWorld(vpos);
if (true) { //frustum.containsPoint(vpos)) {
var dsq = vpos.distanceToSquared(campos);
if (dsq < near) {
near = dsq;
nearnode = within[n];
}
}
}
}
}
}
near = Math.max(Math.sqrt(near), 0.00001);
far = Math.max(Math.sqrt(far), 10);
}
//console.log('set near/far:', near, far, (nearnode && nearnode.userData.thing ? nearnode.userData.thing.name : nearnode), (farnode && farnode.userData.thing ? farnode.userData.thing.name : farnode), nearradius, farradius);
//var nearthing = this.getParentThing(nearnode);
//this.camdebug.setcontent("<ul><li>Near: " + near + "</li><li>Far: " + far + "</li><li>Nearest Object: " + (nearthing ? nearthing.name : '(unknown:' + (nearnode ? nearnode.name || nearnode.id : "") + ')') + "</li></ul>");
if (near != Infinity && far != 0) {
//this.camera.near = near * .5;
this.camera.far = far * 1.2;
this.camera.updateProjectionMatrix();
}
}
setskyscene(scene) {
this.skyscene = scene || new THREE.Scene();
this.skycamera = new THREE.PerspectiveCamera(this.camera.fov, this.camera.aspect, 0.1, 10000);
//this.skycamera.rotation = this.camera.rotation;
//this.skycamera.quaternion = this.camera.quaternion;
this.skyscene.add(this.skycamera);
if (!this.skypass) {
this.skypass = this.createRenderPass('sky');
this.composer.passes[0].clear = false;
this.composer.passes.unshift(this.skypass);
}
}
getscene(obj) {
var scene = obj;
while ( scene.parent ) {
scene = scene.parent;
}
if ( scene !== undefined && scene instanceof THREE.Scene ) {
return scene;
}
return false;
}
getsize(force) {
//this.size = [this.offsetWidth, this.offsetHeight];
var s = (this.fullsize ? {w: window.innerWidth, h: window.innerHeight, bar: 2} :
(this.resolution ? {w: this.resolution[0], h: this.resolution[1], foo: 1} :
elation.html.dimensions(this)
));
if (this.xrsession) {
let xrlayer = this.getXRBaseLayer(this.xrsession);
if (xrlayer) {
s = {
w: xrlayer.framebufferWidth,
h: xrlayer.framebufferHeight,
};
}
}
var domel = this.rendersystem.renderer.domElement;
if (domel && (force || ((s.w != this.size[0] || s.h != this.size[1]) && (s.w > 0 && s.h > 0)))) {
this.size = [s.w, s.h];
this.setrendersize(this.size[0], this.size[1]);
}
this.actualcamera.zoom = (Math.abs(window.orientation) == 90 ? 2 : 1);
this.actualcamera.updateProjectionMatrix();
this.rendersystem.setdirty();
return this.size;
}
setrendersize(width, height) {
if (this.xrsession) return;
var scale = this.scale / 100,
invscale = 100 / this.scale,
scaledwidth = width * scale,
scaledheight = height * scale;
if (scaledwidth == 0 || scaledheight == 0) {
console.warn('Renderer was told to resize to ' + scaledwidth + 'x' + scaledheight);
return;
}
this.rendersystem.renderer.domElement.style.transformOrigin = '0 0';
this.rendersystem.renderer.domElement.style.transform = 'scale3d(' + [invscale, invscale, invscale].join(',') + ')';
this.sizevec.set(scaledwidth, scaledheight);
this.sizevecinverse.set(1/scaledwidth, 1/scaledheight);
var pixelratio = 1; //(window.devicePixelRatio ? window.devicePixelRatio : 1);
if (pixelratio != this.rendersystem.renderer.getPixelRatio()) {
this.rendersystem.renderer.setPixelRatio(pixelratio);
}
this.rendersystem.renderer.setSize(scaledwidth, scaledheight, false);
if (this.composer) {
this.composer.setSize(scaledwidth, scaledheight);
}
if (this.rendersystem.cssrenderer) {
this.rendersystem.cssrenderer.setSize(width, height);
//this.rendersystem.cssrenderer.setPixelRatio(pixelratio);
}
//this.composer.setSize(scaledwidth, scaledheight);
if (this.pickingcomposer) {
this.pickingcomposer.setSize(scaledwidth, scaledheight);
}
if (this.depthTarget) {
this.depthTarget.setSize(scaledwidth, scaledheight);
}
if (this.effects['SSAO']) {
this.effects['SSAO'].uniforms[ 'size' ].value.set( width, height);
this.effects['SSAO'].uniforms[ 'tDepth' ].value = this.depthTarget;
}
if (this.skycamera) {
this.skycamera.aspect = width / height / this.aspectscale;
this.skycamera.updateProjectionMatrix() / this.aspectscale;
}
if (this.camera) {
this.camera.aspect = width / height / this.aspectscale;
this.camera.updateProjectionMatrix();
}
if (this.actualcamera) {
this.actualcamera.aspect = width / height;
this.actualcamera.updateProjectionMatrix();
}
}
setscale(scale) {
this.scale = scale;
}
system_attach(ev) {
console.log('INIT: view (' + this.id + ')');
}
engine_start(ev) {
}
engine_frame(ev) {
//var scene = this.engine.systems.world.scene['world-3d'];
//console.log('FRAME: view (' + this.id + ")");
}
engine_stop(ev) {
console.log('SHUTDOWN: view (' + this.id + ')');
}
handleEvent(ev) {
if (typeof this[ev.type] == 'function') return this[ev.type](ev);
}
resize(ev) {
this.getsize(ev.data);
}
mouseover(ev) {
if (!this.pickingactive) {
//elation.events.add(this, 'mousemove,mouseout', this);
this.pickingactive = true;
}
this.mousepos = [ev.clientX, ev.clientY, document.body.scrollTop];
}
mousedown(ev) {
if (this.pickingactive && this.picker.pickingobject) {
this.cancelclick = false;
var newev = {type: 'mousedown', element: this.getParentThing(this.picker.pickingobject), data: this.getPickingData(this.picker.pickingobject, [ev.clientX, ev.clientY]), clientX: ev.clientX, clientY: ev.clientY, button: ev.button, shiftKey: ev.shiftKey, altKey: ev.altKey, ctrlKey: ev.ctrlKey, metaKey: ev.metaKey};
/*
var fired = elation.events.fire(newev);
for (var i = 0; i < fired.length; i++) {
if (fired[i].cancelBubble === true || ev.cancelBubble === true) { ev.stopPropagation(); }
if (fired[i].returnValue === false || ev.returnValue === false) { ev.preventDefault(); }
}
*/
this.proxyEvent(newev);
}
}
mousewheel(ev) {
//this.mousepos[0] = ev.clientX;
//this.mousepos[1] = ev.clientY;
this.mousepos[2] = document.body.scrollTop;
var newev = {type: 'wheel', element: this.getParentThing(this.picker.pickingobject), data: this.getPickingData(this.picker.pickingobject, [ev.clientX, ev.clientY]), clientX: ev.clientX, clientY: ev.clientY, deltaX: ev.deltaX, deltaY: ev.deltaY, deltaZ: ev.deltaZ, deltaMode: ev.deltaMode, shiftKey: ev.shiftKey, altKey: ev.altKey, ctrlKey: ev.ctrlKey, metaKey: ev.metaKey, preventDefault: () => ev.preventDefault(), stopPropagation: () => ev.stopPropagation()};
this.proxyEvent(newev);
if (newev.cancelBubble) {
ev.stopPropagation();
}
if (elation.events.wasDefaultPrevented(newev)) {
ev.preventDefault();
return false;
}
}
mousemove(ev, ignorePointerLock) {
var el = document.pointerLockElement || document.mozPointerLockElement;
if (el && !ignorePointerLock) {
var dims = elation.html.dimensions(el);
this.mousepos[0] = Math.round(dims.w / 2);
this.mousepos[1] = Math.round(dims.h / 2);
this.mousepos[2] = document.body.scrollTop;
if (this.rendermode == 'oculus') {
this.mousepos[0] /= 2;
}
} else if (this.mousepos && this.mousepos[0] != ev.clientX || this.mousepos[1] != ev.clientY) {
this.mousepos[0] = ev.clientX;
this.mousepos[1] = ev.clientY;
this.mousepos[2] = document.body.scrollTop;
//this.cancelclick = true;
}
}
mouseenter(ev) {
this.rendersystem.setdirty();
}
mouseleave(ev) {
this.rendersystem.setdirty();
}
mouseout(ev) {
if (this.pickingactive) {
elation.events.remove(this, 'mousemove,mouseout', this);
this.pickingactive = false;
if (this.picker.pickingobject) {
var newev = {type: "mouseout", element: this.getParentThing(this.picker.pickingobject), data: this.getPickingData(this.picker.pickingobject, [ev.clientX, ev.clientY]), client