import * as echarts from 'echarts/lib/echarts'; import graphicGL from '../../util/graphicGL'; import glmatrix from 'claygl/src/dep/glmatrix'; import trianglesSortMixin from '../../util/geometry/trianglesSortMixin'; import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual'; var vec3 = glmatrix.vec3; function isPointsNaN(pt) { return isNaN(pt[0]) || isNaN(pt[1]) || isNaN(pt[2]); } export default echarts.ChartView.extend({ type: 'surface', __ecgl__: true, init: function (ecModel, api) { this.groupGL = new graphicGL.Node(); }, render: function (seriesModel, ecModel, api) { // Swap surfaceMesh var tmp = this._prevSurfaceMesh; this._prevSurfaceMesh = this._surfaceMesh; this._surfaceMesh = tmp; if (!this._surfaceMesh) { this._surfaceMesh = this._createSurfaceMesh(); } this.groupGL.remove(this._prevSurfaceMesh); this.groupGL.add(this._surfaceMesh); var coordSys = seriesModel.coordinateSystem; var shading = seriesModel.get('shading'); var data = seriesModel.getData(); var shadingPrefix = 'ecgl.' + shading; if (!this._surfaceMesh.material || this._surfaceMesh.material.shader.name !== shadingPrefix) { this._surfaceMesh.material = graphicGL.createMaterial(shadingPrefix, ['VERTEX_COLOR', 'DOUBLE_SIDED']); } graphicGL.setMaterialFromModel(shading, this._surfaceMesh.material, seriesModel, api); if (coordSys && coordSys.viewGL) { coordSys.viewGL.add(this.groupGL); var methodName = coordSys.viewGL.isLinearSpace() ? 'define' : 'undefine'; this._surfaceMesh.material[methodName]('fragment', 'SRGB_DECODE'); } var isParametric = seriesModel.get('parametric'); var dataShape = seriesModel.get('dataShape'); if (!dataShape) { dataShape = this._getDataShape(data, isParametric); if (process.env.NODE_ENV !== 'production') { if (seriesModel.get('data')) { console.warn('dataShape is not provided. Guess it is ', dataShape); } } } var wireframeModel = seriesModel.getModel('wireframe'); var wireframeLineWidth = wireframeModel.get('lineStyle.width'); var showWireframe = wireframeModel.get('show') && wireframeLineWidth > 0; this._updateSurfaceMesh(this._surfaceMesh, seriesModel, dataShape, showWireframe); var material = this._surfaceMesh.material; if (showWireframe) { material.define('WIREFRAME_QUAD'); material.set('wireframeLineWidth', wireframeLineWidth); material.set('wireframeLineColor', graphicGL.parseColor(wireframeModel.get('lineStyle.color'))); } else { material.undefine('WIREFRAME_QUAD'); } this._initHandler(seriesModel, api); this._updateAnimation(seriesModel); }, _updateAnimation: function (seriesModel) { graphicGL.updateVertexAnimation([['prevPosition', 'position'], ['prevNormal', 'normal']], this._prevSurfaceMesh, this._surfaceMesh, seriesModel); }, _createSurfaceMesh: function () { var mesh = new graphicGL.Mesh({ geometry: new graphicGL.Geometry({ dynamic: true, sortTriangles: true }), shadowDepthMaterial: new graphicGL.Material({ shader: new graphicGL.Shader(graphicGL.Shader.source('ecgl.sm.depth.vertex'), graphicGL.Shader.source('ecgl.sm.depth.fragment')) }), culling: false, // Render after axes renderOrder: 10, // Render normal in normal pass renderNormal: true }); mesh.geometry.createAttribute('barycentric', 'float', 4); mesh.geometry.createAttribute('prevPosition', 'float', 3); mesh.geometry.createAttribute('prevNormal', 'float', 3); Object.assign(mesh.geometry, trianglesSortMixin); return mesh; }, _initHandler: function (seriesModel, api) { var data = seriesModel.getData(); var surfaceMesh = this._surfaceMesh; var coordSys = seriesModel.coordinateSystem; function getNearestPointIdx(triangle, point) { var nearestDist = Infinity; var nearestIdx = -1; var pos = []; for (var i = 0; i < triangle.length; i++) { surfaceMesh.geometry.attributes.position.get(triangle[i], pos); var dist = vec3.dist(point.array, pos); if (dist < nearestDist) { nearestDist = dist; nearestIdx = triangle[i]; } } return nearestIdx; } surfaceMesh.seriesIndex = seriesModel.seriesIndex; var lastDataIndex = -1; surfaceMesh.off('mousemove'); surfaceMesh.off('mouseout'); surfaceMesh.on('mousemove', function (e) { var idx = getNearestPointIdx(e.triangle, e.point); if (idx >= 0) { var point = []; surfaceMesh.geometry.attributes.position.get(idx, point); var value = coordSys.pointToData(point); var minDist = Infinity; var dataIndex = -1; var item = []; for (var i = 0; i < data.count(); i++) { item[0] = data.get('x', i); item[1] = data.get('y', i); item[2] = data.get('z', i); var dist = vec3.squaredDistance(item, value); if (dist < minDist) { dataIndex = i; minDist = dist; } } if (dataIndex !== lastDataIndex) { api.dispatchAction({ type: 'grid3DShowAxisPointer', value: value }); } lastDataIndex = dataIndex; surfaceMesh.dataIndex = dataIndex; } else { surfaceMesh.dataIndex = -1; } }, this); surfaceMesh.on('mouseout', function (e) { lastDataIndex = -1; surfaceMesh.dataIndex = -1; api.dispatchAction({ type: 'grid3DHideAxisPointer' }); }, this); }, _updateSurfaceMesh: function (surfaceMesh, seriesModel, dataShape, showWireframe) { var geometry = surfaceMesh.geometry; var data = seriesModel.getData(); var pointsArr = data.getLayout('points'); var invalidDataCount = 0; data.each(function (idx) { if (!data.hasValue(idx)) { invalidDataCount++; } }); var needsSplitQuad = invalidDataCount || showWireframe; var positionAttr = geometry.attributes.position; var normalAttr = geometry.attributes.normal; var texcoordAttr = geometry.attributes.texcoord0; var barycentricAttr = geometry.attributes.barycentric; var colorAttr = geometry.attributes.color; var row = dataShape[0]; var column = dataShape[1]; var shading = seriesModel.get('shading'); var needsNormal = shading !== 'color'; if (needsSplitQuad) { // TODO, If needs remove the invalid points, or set color transparent. var vertexCount = (row - 1) * (column - 1) * 4; positionAttr.init(vertexCount); if (showWireframe) { barycentricAttr.init(vertexCount); } } else { positionAttr.value = new Float32Array(pointsArr); } colorAttr.init(geometry.vertexCount); texcoordAttr.init(geometry.vertexCount); var quadToTriangle = [0, 3, 1, 1, 3, 2]; // 3----2 // 0----1 // Make sure pixels on 1---3 edge will not have channel 0. // And pixels on four edges have at least one channel 0. var quadBarycentric = [[1, 1, 0, 0], [0, 1, 0, 1], [1, 0, 0, 1], [1, 0, 1, 0]]; var indices = geometry.indices = new (geometry.vertexCount > 0xffff ? Uint32Array : Uint16Array)((row - 1) * (column - 1) * 6); var getQuadIndices = function (i, j, out) { out[1] = i * column + j; out[0] = i * column + j + 1; out[3] = (i + 1) * column + j + 1; out[2] = (i + 1) * column + j; }; var isTransparent = false; if (needsSplitQuad) { var quadIndices = []; var pos = []; var faceOffset = 0; if (needsNormal) { normalAttr.init(geometry.vertexCount); } else { normalAttr.value = null; } var pts = [[], [], []]; var v21 = [], v32 = []; var normal = vec3.create(); var getFromArray = function (arr, idx, out) { var idx3 = idx * 3; out[0] = arr[idx3]; out[1] = arr[idx3 + 1]; out[2] = arr[idx3 + 2]; return out; }; var vertexNormals = new Float32Array(pointsArr.length); var vertexColors = new Float32Array(pointsArr.length / 3 * 4); for (var i = 0; i < data.count(); i++) { if (data.hasValue(i)) { var rgbaArr = graphicGL.parseColor(getItemVisualColor(data, i)); var opacity = getItemVisualOpacity(data, i); opacity != null && (rgbaArr[3] *= opacity); if (rgbaArr[3] < 0.99) { isTransparent = true; } for (var k = 0; k < 4; k++) { vertexColors[i * 4 + k] = rgbaArr[k]; } } } var farPoints = [1e7, 1e7, 1e7]; for (var i = 0; i < row - 1; i++) { for (var j = 0; j < column - 1; j++) { var dataIndex = i * (column - 1) + j; var vertexOffset = dataIndex * 4; getQuadIndices(i, j, quadIndices); var invisibleQuad = false; for (var k = 0; k < 4; k++) { getFromArray(pointsArr, quadIndices[k], pos); if (isPointsNaN(pos)) { // Quad is invisible if any point is NaN invisibleQuad = true; } } for (var k = 0; k < 4; k++) { if (invisibleQuad) { // Move point far away positionAttr.set(vertexOffset + k, farPoints); } else { getFromArray(pointsArr, quadIndices[k], pos); positionAttr.set(vertexOffset + k, pos); } if (showWireframe) { barycentricAttr.set(vertexOffset + k, quadBarycentric[k]); } } for (var k = 0; k < 6; k++) { indices[faceOffset++] = quadToTriangle[k] + vertexOffset; } // Vertex normals if (needsNormal && !invisibleQuad) { for (var k = 0; k < 2; k++) { var k3 = k * 3; for (var m = 0; m < 3; m++) { var idx = quadIndices[quadToTriangle[k3] + m]; getFromArray(pointsArr, idx, pts[m]); } vec3.sub(v21, pts[0], pts[1]); vec3.sub(v32, pts[1], pts[2]); vec3.cross(normal, v21, v32); // Weighted by the triangle area for (var m = 0; m < 3; m++) { var idx3 = quadIndices[quadToTriangle[k3] + m] * 3; vertexNormals[idx3] = vertexNormals[idx3] + normal[0]; vertexNormals[idx3 + 1] = vertexNormals[idx3 + 1] + normal[1]; vertexNormals[idx3 + 2] = vertexNormals[idx3 + 2] + normal[2]; } } } } } if (needsNormal) { for (var i = 0; i < vertexNormals.length / 3; i++) { getFromArray(vertexNormals, i, normal); vec3.normalize(normal, normal); vertexNormals[i * 3] = normal[0]; vertexNormals[i * 3 + 1] = normal[1]; vertexNormals[i * 3 + 2] = normal[2]; } } // Split normal and colors, write to the attributes. var rgbaArr = []; var uvArr = []; for (var i = 0; i < row - 1; i++) { for (var j = 0; j < column - 1; j++) { var dataIndex = i * (column - 1) + j; var vertexOffset = dataIndex * 4; getQuadIndices(i, j, quadIndices); for (var k = 0; k < 4; k++) { for (var m = 0; m < 4; m++) { rgbaArr[m] = vertexColors[quadIndices[k] * 4 + m]; } colorAttr.set(vertexOffset + k, rgbaArr); if (needsNormal) { getFromArray(vertexNormals, quadIndices[k], normal); normalAttr.set(vertexOffset + k, normal); } var idx = quadIndices[k]; uvArr[0] = idx % column / (column - 1); uvArr[1] = Math.floor(idx / column) / (row - 1); texcoordAttr.set(vertexOffset + k, uvArr); } dataIndex++; } } } else { var uvArr = []; for (var i = 0; i < data.count(); i++) { uvArr[0] = i % column / (column - 1); uvArr[1] = Math.floor(i / column) / (row - 1); var rgbaArr = graphicGL.parseColor(getItemVisualColor(data, i)); var opacity = getItemVisualOpacity(data, i); opacity != null && (rgbaArr[3] *= opacity); if (rgbaArr[3] < 0.99) { isTransparent = true; } colorAttr.set(i, rgbaArr); texcoordAttr.set(i, uvArr); } var quadIndices = []; // Triangles var cursor = 0; for (var i = 0; i < row - 1; i++) { for (var j = 0; j < column - 1; j++) { getQuadIndices(i, j, quadIndices); for (var k = 0; k < 6; k++) { indices[cursor++] = quadIndices[quadToTriangle[k]]; } } } if (needsNormal) { geometry.generateVertexNormals(); } else { normalAttr.value = null; } } if (surfaceMesh.material.get('normalMap')) { geometry.generateTangents(); } geometry.updateBoundingBox(); geometry.dirty(); surfaceMesh.material.transparent = isTransparent; surfaceMesh.material.depthMask = !isTransparent; }, _getDataShape: function (data, isParametric) { var prevX = -Infinity; var rowCount = 0; var columnCount = 0; var prevColumnCount = 0; var mayInvalid = false; var rowDim = isParametric ? 'u' : 'x'; var dataCount = data.count(); // Check data format for (var i = 0; i < dataCount; i++) { var x = data.get(rowDim, i); if (x < prevX) { if (prevColumnCount && prevColumnCount !== columnCount) { if (process.env.NODE_ENV !== 'production') { mayInvalid = true; } } // A new row. prevColumnCount = columnCount; columnCount = 0; rowCount++; } prevX = x; columnCount++; } if (!rowCount || columnCount === 1) { mayInvalid = true; } if (!mayInvalid) { return [rowCount + 1, columnCount]; } var rows = Math.floor(Math.sqrt(dataCount)); while (rows > 0) { if (Math.floor(dataCount / rows) === dataCount / rows) { // Can be divided return [rows, dataCount / rows]; } rows--; } // Bailout rows = Math.floor(Math.sqrt(dataCount)); return [rows, rows]; }, dispose: function () { this.groupGL.removeAll(); }, remove: function () { this.groupGL.removeAll(); } });