713 lines
16 KiB
JavaScript
713 lines
16 KiB
JavaScript
/**
|
|
* Provide WebGL layer to zrender. Which is rendered on top of clay.
|
|
*
|
|
*
|
|
* Relationship between zrender, LayerGL(renderer) and ViewGL(Scene, Camera, Viewport)
|
|
* zrender
|
|
* / \
|
|
* LayerGL LayerGL
|
|
* (renderer) (renderer)
|
|
* / \
|
|
* ViewGL ViewGL
|
|
*/
|
|
import * as echarts from 'echarts/lib/echarts';
|
|
import Renderer from 'claygl/src/Renderer';
|
|
import RayPicking from 'claygl/src/picking/RayPicking';
|
|
import Texture from 'claygl/src/Texture';
|
|
import graphicGL from '../util/graphicGL'; // PENDING, clay. notifier is same with zrender Eventful
|
|
|
|
import notifier from 'claygl/src/core/mixin/notifier';
|
|
import requestAnimationFrame from 'zrender/lib/animation/requestAnimationFrame';
|
|
/**
|
|
* @constructor
|
|
* @alias module:echarts-gl/core/LayerGL
|
|
* @param {string} id Layer ID
|
|
* @param {module:zrender/ZRender} zr
|
|
*/
|
|
|
|
var LayerGL = function (id, zr) {
|
|
/**
|
|
* Layer ID
|
|
* @type {string}
|
|
*/
|
|
this.id = id;
|
|
/**
|
|
* @type {module:zrender/ZRender}
|
|
*/
|
|
|
|
this.zr = zr;
|
|
/**
|
|
* @type {clay.Renderer}
|
|
*/
|
|
|
|
try {
|
|
this.renderer = new Renderer({
|
|
clearBit: 0,
|
|
devicePixelRatio: zr.painter.dpr,
|
|
preserveDrawingBuffer: true,
|
|
// PENDING
|
|
premultipliedAlpha: true
|
|
});
|
|
this.renderer.resize(zr.painter.getWidth(), zr.painter.getHeight());
|
|
} catch (e) {
|
|
this.renderer = null;
|
|
this.dom = document.createElement('div');
|
|
this.dom.style.cssText = 'position:absolute; left: 0; top: 0; right: 0; bottom: 0;';
|
|
this.dom.className = 'ecgl-nowebgl';
|
|
this.dom.innerHTML = 'Sorry, your browser does not support WebGL';
|
|
console.error(e);
|
|
return;
|
|
}
|
|
|
|
this.onglobalout = this.onglobalout.bind(this);
|
|
zr.on('globalout', this.onglobalout);
|
|
/**
|
|
* Canvas dom for webgl rendering
|
|
* @type {HTMLCanvasElement}
|
|
*/
|
|
|
|
this.dom = this.renderer.canvas;
|
|
var style = this.dom.style;
|
|
style.position = 'absolute';
|
|
style.left = '0';
|
|
style.top = '0';
|
|
/**
|
|
* @type {Array.<clay.Scene>}
|
|
*/
|
|
|
|
this.views = [];
|
|
this._picking = new RayPicking({
|
|
renderer: this.renderer
|
|
});
|
|
this._viewsToDispose = [];
|
|
/**
|
|
* Current accumulating id.
|
|
*/
|
|
|
|
this._accumulatingId = 0;
|
|
this._zrEventProxy = new echarts.graphic.Rect({
|
|
shape: {
|
|
x: -1,
|
|
y: -1,
|
|
width: 2,
|
|
height: 2
|
|
},
|
|
// FIXME Better solution.
|
|
__isGLToZRProxy: true
|
|
});
|
|
this._backgroundColor = null;
|
|
this._disposed = false;
|
|
};
|
|
|
|
LayerGL.prototype.setUnpainted = function () {};
|
|
/**
|
|
* @param {module:echarts-gl/core/ViewGL} view
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.addView = function (view) {
|
|
if (view.layer === this) {
|
|
return;
|
|
} // If needs to dispose in this layer. unmark it.
|
|
|
|
|
|
var idx = this._viewsToDispose.indexOf(view);
|
|
|
|
if (idx >= 0) {
|
|
this._viewsToDispose.splice(idx, 1);
|
|
}
|
|
|
|
this.views.push(view);
|
|
view.layer = this;
|
|
var zr = this.zr;
|
|
view.scene.traverse(function (node) {
|
|
node.__zr = zr;
|
|
|
|
if (node.addAnimatorsToZr) {
|
|
node.addAnimatorsToZr(zr);
|
|
}
|
|
});
|
|
};
|
|
|
|
function removeFromZr(node) {
|
|
var zr = node.__zr;
|
|
node.__zr = null;
|
|
|
|
if (zr && node.removeAnimatorsFromZr) {
|
|
node.removeAnimatorsFromZr(zr);
|
|
}
|
|
}
|
|
/**
|
|
* @param {module:echarts-gl/core/ViewGL} view
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.removeView = function (view) {
|
|
if (view.layer !== this) {
|
|
return;
|
|
}
|
|
|
|
var idx = this.views.indexOf(view);
|
|
|
|
if (idx >= 0) {
|
|
this.views.splice(idx, 1);
|
|
view.scene.traverse(removeFromZr, this);
|
|
view.layer = null; // Mark to dispose in this layer.
|
|
|
|
this._viewsToDispose.push(view);
|
|
}
|
|
};
|
|
/**
|
|
* Remove all views
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.removeViewsAll = function () {
|
|
this.views.forEach(function (view) {
|
|
view.scene.traverse(removeFromZr, this);
|
|
view.layer = null; // Mark to dispose in this layer.
|
|
|
|
this._viewsToDispose.push(view);
|
|
}, this);
|
|
this.views.length = 0;
|
|
};
|
|
/**
|
|
* Resize the canvas and viewport, will be invoked by zrender
|
|
* @param {number} width
|
|
* @param {number} height
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.resize = function (width, height) {
|
|
var renderer = this.renderer;
|
|
renderer.resize(width, height);
|
|
};
|
|
/**
|
|
* Clear color and depth
|
|
* @return {[type]} [description]
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.clear = function () {
|
|
var gl = this.renderer.gl;
|
|
var clearColor = this._backgroundColor || [0, 0, 0, 0];
|
|
gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]);
|
|
gl.depthMask(true);
|
|
gl.colorMask(true, true, true, true);
|
|
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT);
|
|
};
|
|
/**
|
|
* Clear depth
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.clearDepth = function () {
|
|
var gl = this.renderer.gl;
|
|
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
};
|
|
/**
|
|
* Clear color
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.clearColor = function () {
|
|
var gl = this.renderer.gl;
|
|
gl.clearColor(0, 0, 0, 0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
};
|
|
/**
|
|
* Mark layer to refresh next tick
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.needsRefresh = function () {
|
|
this.zr.refresh();
|
|
};
|
|
/**
|
|
* Refresh the layer, will be invoked by zrender
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.refresh = function (bgColor) {
|
|
this._backgroundColor = bgColor ? graphicGL.parseColor(bgColor) : [0, 0, 0, 0];
|
|
this.renderer.clearColor = this._backgroundColor;
|
|
|
|
for (var i = 0; i < this.views.length; i++) {
|
|
this.views[i].prepareRender(this.renderer);
|
|
}
|
|
|
|
this._doRender(false); // Auto dispose unused resources on GPU, like program(shader), texture, geometry(buffers)
|
|
|
|
|
|
this._trackAndClean(); // Dispose trashed views
|
|
|
|
|
|
for (var i = 0; i < this._viewsToDispose.length; i++) {
|
|
this._viewsToDispose[i].dispose(this.renderer);
|
|
}
|
|
|
|
this._viewsToDispose.length = 0;
|
|
|
|
this._startAccumulating();
|
|
};
|
|
|
|
LayerGL.prototype.renderToCanvas = function (ctx) {
|
|
// PENDING will block the page
|
|
this._startAccumulating(true);
|
|
|
|
ctx.drawImage(this.dom, 0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
};
|
|
|
|
LayerGL.prototype._doRender = function (accumulating) {
|
|
this.clear();
|
|
this.renderer.saveViewport();
|
|
|
|
for (var i = 0; i < this.views.length; i++) {
|
|
this.views[i].render(this.renderer, accumulating);
|
|
}
|
|
|
|
this.renderer.restoreViewport();
|
|
};
|
|
/**
|
|
* Stop accumulating
|
|
*/
|
|
|
|
|
|
LayerGL.prototype._stopAccumulating = function () {
|
|
this._accumulatingId = 0;
|
|
clearTimeout(this._accumulatingTimeout);
|
|
};
|
|
|
|
var accumulatingId = 1;
|
|
/**
|
|
* Start accumulating all the views.
|
|
* Accumulating is for antialising and have more sampling in SSAO
|
|
* @private
|
|
*/
|
|
|
|
LayerGL.prototype._startAccumulating = function (immediate) {
|
|
var self = this;
|
|
|
|
this._stopAccumulating();
|
|
|
|
var needsAccumulate = false;
|
|
|
|
for (var i = 0; i < this.views.length; i++) {
|
|
needsAccumulate = this.views[i].needsAccumulate() || needsAccumulate;
|
|
}
|
|
|
|
if (!needsAccumulate) {
|
|
return;
|
|
}
|
|
|
|
function accumulate(id) {
|
|
if (!self._accumulatingId || id !== self._accumulatingId) {
|
|
return;
|
|
}
|
|
|
|
var isFinished = true;
|
|
|
|
for (var i = 0; i < self.views.length; i++) {
|
|
isFinished = self.views[i].isAccumulateFinished() && needsAccumulate;
|
|
}
|
|
|
|
if (!isFinished) {
|
|
self._doRender(true);
|
|
|
|
if (immediate) {
|
|
accumulate(id);
|
|
} else {
|
|
requestAnimationFrame(function () {
|
|
accumulate(id);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
this._accumulatingId = accumulatingId++;
|
|
|
|
if (immediate) {
|
|
accumulate(self._accumulatingId);
|
|
} else {
|
|
this._accumulatingTimeout = setTimeout(function () {
|
|
accumulate(self._accumulatingId);
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
LayerGL.prototype._trackAndClean = function () {
|
|
var textureList = [];
|
|
var geometriesList = []; // Mark all resources unused;
|
|
|
|
if (this._textureList) {
|
|
markUnused(this._textureList);
|
|
markUnused(this._geometriesList);
|
|
}
|
|
|
|
for (var i = 0; i < this.views.length; i++) {
|
|
collectResources(this.views[i].scene, textureList, geometriesList);
|
|
} // Dispose those unsed resources.
|
|
|
|
|
|
if (this._textureList) {
|
|
checkAndDispose(this.renderer, this._textureList);
|
|
checkAndDispose(this.renderer, this._geometriesList);
|
|
}
|
|
|
|
this._textureList = textureList;
|
|
this._geometriesList = geometriesList;
|
|
};
|
|
|
|
function markUnused(resourceList) {
|
|
for (var i = 0; i < resourceList.length; i++) {
|
|
resourceList[i].__used__ = 0;
|
|
}
|
|
}
|
|
|
|
function checkAndDispose(renderer, resourceList) {
|
|
for (var i = 0; i < resourceList.length; i++) {
|
|
if (!resourceList[i].__used__) {
|
|
resourceList[i].dispose(renderer);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateUsed(resource, list) {
|
|
resource.__used__ = resource.__used__ || 0;
|
|
resource.__used__++;
|
|
|
|
if (resource.__used__ === 1) {
|
|
// Don't push to the list twice.
|
|
list.push(resource);
|
|
}
|
|
}
|
|
|
|
function collectResources(scene, textureResourceList, geometryResourceList) {
|
|
var prevMaterial;
|
|
var prevGeometry;
|
|
scene.traverse(function (renderable) {
|
|
if (renderable.isRenderable()) {
|
|
var geometry = renderable.geometry;
|
|
var material = renderable.material; // TODO optimize!!
|
|
|
|
if (material !== prevMaterial) {
|
|
var textureUniforms = material.getTextureUniforms();
|
|
|
|
for (var u = 0; u < textureUniforms.length; u++) {
|
|
var uniformName = textureUniforms[u];
|
|
var val = material.uniforms[uniformName].value;
|
|
|
|
if (!val) {
|
|
continue;
|
|
}
|
|
|
|
if (val instanceof Texture) {
|
|
updateUsed(val, textureResourceList);
|
|
} else if (val instanceof Array) {
|
|
for (var k = 0; k < val.length; k++) {
|
|
if (val[k] instanceof Texture) {
|
|
updateUsed(val[k], textureResourceList);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (geometry !== prevGeometry) {
|
|
updateUsed(geometry, geometryResourceList);
|
|
}
|
|
|
|
prevMaterial = material;
|
|
prevGeometry = geometry;
|
|
}
|
|
});
|
|
|
|
for (var k = 0; k < scene.lights.length; k++) {
|
|
// Track AmbientCubemap
|
|
if (scene.lights[k].cubemap) {
|
|
updateUsed(scene.lights[k].cubemap, textureResourceList);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Dispose the layer
|
|
*/
|
|
|
|
|
|
LayerGL.prototype.dispose = function () {
|
|
if (this._disposed) {
|
|
return;
|
|
}
|
|
|
|
this._stopAccumulating();
|
|
|
|
if (this._textureList) {
|
|
markUnused(this._textureList);
|
|
markUnused(this._geometriesList);
|
|
checkAndDispose(this.renderer, this._textureList);
|
|
checkAndDispose(this.renderer, this._geometriesList);
|
|
}
|
|
|
|
this.zr.off('globalout', this.onglobalout);
|
|
this._disposed = true;
|
|
}; // Event handlers
|
|
|
|
|
|
LayerGL.prototype.onmousedown = function (e) {
|
|
if (e.target && e.target.__isGLToZRProxy) {
|
|
return;
|
|
}
|
|
|
|
e = e.event;
|
|
var obj = this.pickObject(e.offsetX, e.offsetY);
|
|
|
|
if (obj) {
|
|
this._dispatchEvent('mousedown', e, obj);
|
|
|
|
this._dispatchDataEvent('mousedown', e, obj);
|
|
}
|
|
|
|
this._downX = e.offsetX;
|
|
this._downY = e.offsetY;
|
|
};
|
|
|
|
LayerGL.prototype.onmousemove = function (e) {
|
|
if (e.target && e.target.__isGLToZRProxy) {
|
|
return;
|
|
}
|
|
|
|
e = e.event;
|
|
var obj = this.pickObject(e.offsetX, e.offsetY);
|
|
var target = obj && obj.target;
|
|
var lastHovered = this._hovered;
|
|
this._hovered = obj;
|
|
|
|
if (lastHovered && target !== lastHovered.target) {
|
|
lastHovered.relatedTarget = target;
|
|
|
|
this._dispatchEvent('mouseout', e, lastHovered); // this._dispatchDataEvent('mouseout', e, lastHovered);
|
|
|
|
|
|
this.zr.setCursorStyle('default');
|
|
}
|
|
|
|
this._dispatchEvent('mousemove', e, obj);
|
|
|
|
if (obj) {
|
|
this.zr.setCursorStyle('pointer');
|
|
|
|
if (!lastHovered || target !== lastHovered.target) {
|
|
this._dispatchEvent('mouseover', e, obj); // this._dispatchDataEvent('mouseover', e, obj);
|
|
|
|
}
|
|
}
|
|
|
|
this._dispatchDataEvent('mousemove', e, obj);
|
|
};
|
|
|
|
LayerGL.prototype.onmouseup = function (e) {
|
|
if (e.target && e.target.__isGLToZRProxy) {
|
|
return;
|
|
}
|
|
|
|
e = e.event;
|
|
var obj = this.pickObject(e.offsetX, e.offsetY);
|
|
|
|
if (obj) {
|
|
this._dispatchEvent('mouseup', e, obj);
|
|
|
|
this._dispatchDataEvent('mouseup', e, obj);
|
|
}
|
|
|
|
this._upX = e.offsetX;
|
|
this._upY = e.offsetY;
|
|
};
|
|
|
|
LayerGL.prototype.onclick = LayerGL.prototype.dblclick = function (e) {
|
|
if (e.target && e.target.__isGLToZRProxy) {
|
|
return;
|
|
} // Ignore click event if mouse moved
|
|
|
|
|
|
var dx = this._upX - this._downX;
|
|
var dy = this._upY - this._downY;
|
|
|
|
if (Math.sqrt(dx * dx + dy * dy) > 20) {
|
|
return;
|
|
}
|
|
|
|
e = e.event;
|
|
var obj = this.pickObject(e.offsetX, e.offsetY);
|
|
|
|
if (obj) {
|
|
this._dispatchEvent(e.type, e, obj);
|
|
|
|
this._dispatchDataEvent(e.type, e, obj);
|
|
} // Try set depth of field onclick
|
|
|
|
|
|
var result = this._clickToSetFocusPoint(e);
|
|
|
|
if (result) {
|
|
var success = result.view.setDOFFocusOnPoint(result.distance);
|
|
|
|
if (success) {
|
|
this.zr.refresh();
|
|
}
|
|
}
|
|
};
|
|
|
|
LayerGL.prototype._clickToSetFocusPoint = function (e) {
|
|
var renderer = this.renderer;
|
|
var oldViewport = renderer.viewport;
|
|
|
|
for (var i = this.views.length - 1; i >= 0; i--) {
|
|
var viewGL = this.views[i];
|
|
|
|
if (viewGL.hasDOF() && viewGL.containPoint(e.offsetX, e.offsetY)) {
|
|
this._picking.scene = viewGL.scene;
|
|
this._picking.camera = viewGL.camera; // Only used for picking, renderer.setViewport will also invoke gl.viewport.
|
|
// Set directly, PENDING.
|
|
|
|
renderer.viewport = viewGL.viewport;
|
|
|
|
var result = this._picking.pick(e.offsetX, e.offsetY, true);
|
|
|
|
if (result) {
|
|
result.view = viewGL;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
renderer.viewport = oldViewport;
|
|
};
|
|
|
|
LayerGL.prototype.onglobalout = function (e) {
|
|
var lastHovered = this._hovered;
|
|
|
|
if (lastHovered) {
|
|
this._dispatchEvent('mouseout', e, {
|
|
target: lastHovered.target
|
|
});
|
|
}
|
|
};
|
|
|
|
LayerGL.prototype.pickObject = function (x, y) {
|
|
var output = [];
|
|
var renderer = this.renderer;
|
|
var oldViewport = renderer.viewport;
|
|
|
|
for (var i = 0; i < this.views.length; i++) {
|
|
var viewGL = this.views[i];
|
|
|
|
if (viewGL.containPoint(x, y)) {
|
|
this._picking.scene = viewGL.scene;
|
|
this._picking.camera = viewGL.camera; // Only used for picking, renderer.setViewport will also invoke gl.viewport.
|
|
// Set directly, PENDING.
|
|
|
|
renderer.viewport = viewGL.viewport;
|
|
|
|
this._picking.pickAll(x, y, output);
|
|
}
|
|
}
|
|
|
|
renderer.viewport = oldViewport;
|
|
output.sort(function (a, b) {
|
|
return a.distance - b.distance;
|
|
});
|
|
return output[0];
|
|
};
|
|
|
|
LayerGL.prototype._dispatchEvent = function (eveName, originalEvent, newEvent) {
|
|
if (!newEvent) {
|
|
newEvent = {};
|
|
}
|
|
|
|
var current = newEvent.target;
|
|
newEvent.cancelBubble = false;
|
|
newEvent.event = originalEvent;
|
|
newEvent.type = eveName;
|
|
newEvent.offsetX = originalEvent.offsetX;
|
|
newEvent.offsetY = originalEvent.offsetY;
|
|
|
|
while (current) {
|
|
current.trigger(eveName, newEvent);
|
|
current = current.getParent();
|
|
|
|
if (newEvent.cancelBubble) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this._dispatchToView(eveName, newEvent);
|
|
};
|
|
|
|
LayerGL.prototype._dispatchDataEvent = function (eveName, originalEvent, newEvent) {
|
|
var mesh = newEvent && newEvent.target;
|
|
var dataIndex = mesh && mesh.dataIndex;
|
|
var seriesIndex = mesh && mesh.seriesIndex; // Custom event data
|
|
|
|
var eventData = mesh && mesh.eventData;
|
|
var elChangedInMouseMove = false;
|
|
var eventProxy = this._zrEventProxy;
|
|
eventProxy.x = originalEvent.offsetX;
|
|
eventProxy.y = originalEvent.offsetY;
|
|
eventProxy.update();
|
|
var targetInfo = {
|
|
target: eventProxy
|
|
};
|
|
const ecData = echarts.helper.getECData(eventProxy);
|
|
|
|
if (eveName === 'mousemove') {
|
|
if (dataIndex != null) {
|
|
if (dataIndex !== this._lastDataIndex) {
|
|
if (parseInt(this._lastDataIndex, 10) >= 0) {
|
|
ecData.dataIndex = this._lastDataIndex;
|
|
ecData.seriesIndex = this._lastSeriesIndex; // FIXME May cause double events.
|
|
|
|
this.zr.handler.dispatchToElement(targetInfo, 'mouseout', originalEvent);
|
|
}
|
|
|
|
elChangedInMouseMove = true;
|
|
}
|
|
} else if (eventData != null) {
|
|
if (eventData !== this._lastEventData) {
|
|
if (this._lastEventData != null) {
|
|
ecData.eventData = this._lastEventData; // FIXME May cause double events.
|
|
|
|
this.zr.handler.dispatchToElement(targetInfo, 'mouseout', originalEvent);
|
|
}
|
|
|
|
elChangedInMouseMove = true;
|
|
}
|
|
}
|
|
|
|
this._lastEventData = eventData;
|
|
this._lastDataIndex = dataIndex;
|
|
this._lastSeriesIndex = seriesIndex;
|
|
}
|
|
|
|
ecData.eventData = eventData;
|
|
ecData.dataIndex = dataIndex;
|
|
ecData.seriesIndex = seriesIndex;
|
|
|
|
if (eventData != null || parseInt(dataIndex, 10) >= 0 && parseInt(seriesIndex, 10) >= 0) {
|
|
this.zr.handler.dispatchToElement(targetInfo, eveName, originalEvent);
|
|
|
|
if (elChangedInMouseMove) {
|
|
this.zr.handler.dispatchToElement(targetInfo, 'mouseover', originalEvent);
|
|
}
|
|
}
|
|
};
|
|
|
|
LayerGL.prototype._dispatchToView = function (eventName, e) {
|
|
for (var i = 0; i < this.views.length; i++) {
|
|
if (this.views[i].containPoint(e.offsetX, e.offsetY)) {
|
|
this.views[i].trigger(eventName, e);
|
|
}
|
|
}
|
|
};
|
|
|
|
Object.assign(LayerGL.prototype, notifier);
|
|
export default LayerGL; |