import * as echarts from 'echarts/lib/echarts'; import graphicGL from '../../util/graphicGL'; import earcut from '../../util/earcut'; import LinesGeo from '../../util/geometry/Lines3D'; import retrieve from '../../util/retrieve'; import glmatrix from 'claygl/src/dep/glmatrix'; import trianglesSortMixin from '../../util/geometry/trianglesSortMixin'; import LabelsBuilder from './LabelsBuilder'; import lines3DGLSL from '../../util/shader/lines3D.glsl.js'; import { getItemVisualColor, getItemVisualOpacity } from '../../util/visual'; var vec3 = glmatrix.vec3; graphicGL.Shader.import(lines3DGLSL); function Geo3DBuilder(api) { this.rootNode = new graphicGL.Node(); // Cache triangulation result this._triangulationResults = {}; this._shadersMap = graphicGL.COMMON_SHADERS.filter(function (shaderName) { return shaderName !== 'shadow'; }).reduce(function (obj, shaderName) { obj[shaderName] = graphicGL.createShader('ecgl.' + shaderName); return obj; }, {}); this._linesShader = graphicGL.createShader('ecgl.meshLines3D'); var groundMaterials = {}; graphicGL.COMMON_SHADERS.forEach(function (shading) { groundMaterials[shading] = new graphicGL.Material({ shader: graphicGL.createShader('ecgl.' + shading) }); }); this._groundMaterials = groundMaterials; this._groundMesh = new graphicGL.Mesh({ geometry: new graphicGL.PlaneGeometry({ dynamic: true }), castShadow: false, renderNormal: true, $ignorePicking: true }); this._groundMesh.rotation.rotateX(-Math.PI / 2); this._labelsBuilder = new LabelsBuilder(512, 512, api); // Give a large render order. this._labelsBuilder.getMesh().renderOrder = 100; this._labelsBuilder.getMesh().material.depthTest = false; this.rootNode.add(this._labelsBuilder.getMesh()); this._initMeshes(); this._api = api; } Geo3DBuilder.prototype = { constructor: Geo3DBuilder, // Which dimension to extrude. Y or Z extrudeY: true, update: function (componentModel, ecModel, api, start, end) { var data = componentModel.getData(); if (start == null) { start = 0; } if (end == null) { end = data.count(); } this._startIndex = start; this._endIndex = end - 1; this._triangulation(componentModel, start, end); var shader = this._getShader(componentModel.get('shading')); this._prepareMesh(componentModel, shader, api, start, end); this.rootNode.updateWorldTransform(); this._updateRegionMesh(componentModel, api, start, end); var coordSys = componentModel.coordinateSystem; // PENDING if (coordSys.type === 'geo3D') { this._updateGroundPlane(componentModel, coordSys, api); } var self = this; this._labelsBuilder.updateData(data, start, end); this._labelsBuilder.getLabelPosition = function (dataIndex, positionDesc, distance) { var name = data.getName(dataIndex); var center; var height = distance; if (coordSys.type === 'geo3D') { var region = coordSys.getRegion(name); if (!region) { return [NaN, NaN, NaN]; } center = region.getCenter(); var pos = coordSys.dataToPoint([center[0], center[1], height]); return pos; } else { var tmp = self._triangulationResults[dataIndex - self._startIndex]; var center = self.extrudeY ? [(tmp.max[0] + tmp.min[0]) / 2, tmp.max[1] + height, (tmp.max[2] + tmp.min[2]) / 2] : [(tmp.max[0] + tmp.min[0]) / 2, (tmp.max[1] + tmp.min[1]) / 2, tmp.max[2] + height]; } }; this._data = data; this._labelsBuilder.updateLabels(); this._updateDebugWireframe(componentModel); // Reset some state. this._lastHoverDataIndex = 0; }, _initMeshes: function () { var self = this; function createPolygonMesh() { var mesh = new graphicGL.Mesh({ name: 'Polygon', material: new graphicGL.Material({ shader: self._shadersMap.lambert }), geometry: new graphicGL.Geometry({ sortTriangles: true, dynamic: true }), // TODO Disable culling culling: false, ignorePicking: true, // Render normal in normal pass renderNormal: true }); Object.assign(mesh.geometry, trianglesSortMixin); return mesh; } var polygonMesh = createPolygonMesh(); var linesMesh = new graphicGL.Mesh({ material: new graphicGL.Material({ shader: this._linesShader }), castShadow: false, ignorePicking: true, $ignorePicking: true, geometry: new LinesGeo({ useNativeLine: false }) }); this.rootNode.add(polygonMesh); this.rootNode.add(linesMesh); polygonMesh.material.define('both', 'VERTEX_COLOR'); polygonMesh.material.define('fragment', 'DOUBLE_SIDED'); this._polygonMesh = polygonMesh; this._linesMesh = linesMesh; this.rootNode.add(this._groundMesh); }, _getShader: function (shading) { var shader = this._shadersMap[shading]; if (!shader) { if (process.env.NODE_ENV !== 'production') { console.warn('Unkown shading ' + shading); } // Default use lambert shader. shader = this._shadersMap.lambert; } shader.__shading = shading; return shader; }, _prepareMesh: function (componentModel, shader, api, start, end) { var polygonVertexCount = 0; var polygonTriangleCount = 0; var linesVertexCount = 0; var linesTriangleCount = 0; // TODO Lines for (var idx = start; idx < end; idx++) { var polyInfo = this._getRegionPolygonInfo(idx); var lineInfo = this._getRegionLinesInfo(idx, componentModel, this._linesMesh.geometry); polygonVertexCount += polyInfo.vertexCount; polygonTriangleCount += polyInfo.triangleCount; linesVertexCount += lineInfo.vertexCount; linesTriangleCount += lineInfo.triangleCount; } var polygonMesh = this._polygonMesh; var polygonGeo = polygonMesh.geometry; ['position', 'normal', 'texcoord0', 'color'].forEach(function (attrName) { polygonGeo.attributes[attrName].init(polygonVertexCount); }); polygonGeo.indices = polygonVertexCount > 0xffff ? new Uint32Array(polygonTriangleCount * 3) : new Uint16Array(polygonTriangleCount * 3); if (polygonMesh.material.shader !== shader) { polygonMesh.material.attachShader(shader, true); } graphicGL.setMaterialFromModel(shader.__shading, polygonMesh.material, componentModel, api); if (linesVertexCount > 0) { this._linesMesh.geometry.resetOffset(); this._linesMesh.geometry.setVertexCount(linesVertexCount); this._linesMesh.geometry.setTriangleCount(linesTriangleCount); } // Indexing data index from vertex index. this._dataIndexOfVertex = new Uint32Array(polygonVertexCount); // Indexing vertex index range from data index this._vertexRangeOfDataIndex = new Uint32Array((end - start) * 2); }, _updateRegionMesh: function (componentModel, api, start, end) { var data = componentModel.getData(); var vertexOffset = 0; var triangleOffset = 0; // Materials configurations. var hasTranparentRegion = false; var polygonMesh = this._polygonMesh; var linesMesh = this._linesMesh; for (var dataIndex = start; dataIndex < end; dataIndex++) { // Get bunch of visual properties. var regionModel = componentModel.getRegionModel(dataIndex); var itemStyleModel = regionModel.getModel('itemStyle'); var color = retrieve.firstNotNull(getItemVisualColor(data, dataIndex), itemStyleModel.get('color'), '#fff'); var opacity = retrieve.firstNotNull(getItemVisualOpacity(data, dataIndex), itemStyleModel.get('opacity'), 1); var colorArr = graphicGL.parseColor(color); var borderColorArr = graphicGL.parseColor(itemStyleModel.get('borderColor')); colorArr[3] *= opacity; borderColorArr[3] *= opacity; var isTransparent = colorArr[3] < 0.99; polygonMesh.material.set('color', [1, 1, 1, 1]); hasTranparentRegion = hasTranparentRegion || isTransparent; var regionHeight = retrieve.firstNotNull(regionModel.get('height', true), componentModel.get('regionHeight')); var newOffsets = this._updatePolygonGeometry(componentModel, polygonMesh.geometry, dataIndex, regionHeight, vertexOffset, triangleOffset, colorArr); for (var i = vertexOffset; i < newOffsets.vertexOffset; i++) { this._dataIndexOfVertex[i] = dataIndex; } this._vertexRangeOfDataIndex[(dataIndex - start) * 2] = vertexOffset; this._vertexRangeOfDataIndex[(dataIndex - start) * 2 + 1] = newOffsets.vertexOffset; vertexOffset = newOffsets.vertexOffset; triangleOffset = newOffsets.triangleOffset; // Update lines. var lineWidth = itemStyleModel.get('borderWidth'); var hasLine = lineWidth > 0; if (hasLine) { lineWidth *= api.getDevicePixelRatio(); this._updateLinesGeometry(linesMesh.geometry, componentModel, dataIndex, regionHeight, lineWidth, componentModel.coordinateSystem.transform); } linesMesh.invisible = !hasLine; linesMesh.material.set({ color: borderColorArr }); } var polygonMesh = this._polygonMesh; polygonMesh.material.transparent = hasTranparentRegion; polygonMesh.material.depthMask = !hasTranparentRegion; polygonMesh.geometry.updateBoundingBox(); polygonMesh.frontFace = this.extrudeY ? graphicGL.Mesh.CCW : graphicGL.Mesh.CW; // Update tangents if (polygonMesh.material.get('normalMap')) { polygonMesh.geometry.generateTangents(); } polygonMesh.seriesIndex = componentModel.seriesIndex; polygonMesh.on('mousemove', this._onmousemove, this); polygonMesh.on('mouseout', this._onmouseout, this); }, _updateDebugWireframe: function (componentModel) { var debugWireframeModel = componentModel.getModel('debug.wireframe'); // TODO Unshow if (debugWireframeModel.get('show')) { var color = graphicGL.parseColor(debugWireframeModel.get('lineStyle.color') || 'rgba(0,0,0,0.5)'); var width = retrieve.firstNotNull(debugWireframeModel.get('lineStyle.width'), 1); // TODO Will cause highlight wrong var mesh = this._polygonMesh; mesh.geometry.generateBarycentric(); mesh.material.define('both', 'WIREFRAME_TRIANGLE'); mesh.material.set('wireframeLineColor', color); mesh.material.set('wireframeLineWidth', width); } }, _onmousemove: function (e) { var dataIndex = this._dataIndexOfVertex[e.triangle[0]]; if (dataIndex == null) { dataIndex = -1; } if (dataIndex !== this._lastHoverDataIndex) { this.downplay(this._lastHoverDataIndex); this.highlight(dataIndex); this._labelsBuilder.updateLabels([dataIndex]); } this._lastHoverDataIndex = dataIndex; this._polygonMesh.dataIndex = dataIndex; }, _onmouseout: function (e) { if (e.target) { this.downplay(this._lastHoverDataIndex); this._lastHoverDataIndex = -1; this._polygonMesh.dataIndex = -1; } this._labelsBuilder.updateLabels([]); }, _updateGroundPlane: function (componentModel, geo3D, api) { var groundModel = componentModel.getModel('groundPlane', componentModel); this._groundMesh.invisible = !groundModel.get('show', true); if (this._groundMesh.invisible) { return; } var shading = componentModel.get('shading'); var material = this._groundMaterials[shading]; if (!material) { if (process.env.NODE_ENV !== 'production') { console.warn('Unkown shading ' + shading); } material = this._groundMaterials.lambert; } graphicGL.setMaterialFromModel(shading, material, groundModel, api); if (material.get('normalMap')) { this._groundMesh.geometry.generateTangents(); } this._groundMesh.material = material; this._groundMesh.material.set('color', graphicGL.parseColor(groundModel.get('color'))); this._groundMesh.scale.set(geo3D.size[0], geo3D.size[2], 1); }, _triangulation: function (componentModel, start, end) { this._triangulationResults = []; var minAll = [Infinity, Infinity, Infinity]; var maxAll = [-Infinity, -Infinity, -Infinity]; var coordSys = componentModel.coordinateSystem; for (var idx = start; idx < end; idx++) { var polygons = []; var polygonCoords = componentModel.getRegionPolygonCoords(idx); for (var i = 0; i < polygonCoords.length; i++) { var exterior = polygonCoords[i].exterior; var interiors = polygonCoords[i].interiors; var points = []; var holes = []; if (exterior.length < 3) { continue; } var offset = 0; for (var j = 0; j < exterior.length; j++) { var p = exterior[j]; points[offset++] = p[0]; points[offset++] = p[1]; } for (var j = 0; j < interiors.length; j++) { if (interiors[j].length < 3) { continue; } var startIdx = points.length / 2; for (var k = 0; k < interiors[j].length; k++) { var p = interiors[j][k]; points.push(p[0]); points.push(p[1]); } holes.push(startIdx); } var triangles = earcut(points, holes); var points3 = new Float64Array(points.length / 2 * 3); var pos = []; var min = [Infinity, Infinity, Infinity]; var max = [-Infinity, -Infinity, -Infinity]; var off3 = 0; for (var j = 0; j < points.length;) { vec3.set(pos, points[j++], 0, points[j++]); if (coordSys && coordSys.transform) { vec3.transformMat4(pos, pos, coordSys.transform); } vec3.min(min, min, pos); vec3.max(max, max, pos); points3[off3++] = pos[0]; points3[off3++] = pos[1]; points3[off3++] = pos[2]; } vec3.min(minAll, minAll, min); vec3.max(maxAll, maxAll, max); polygons.push({ points: points3, indices: triangles, min: min, max: max }); } this._triangulationResults.push(polygons); } this._geoBoundingBox = [minAll, maxAll]; }, /** * Get region vertex and triangle count */ _getRegionPolygonInfo: function (idx) { var polygons = this._triangulationResults[idx - this._startIndex]; var sideVertexCount = 0; var sideTriangleCount = 0; for (var i = 0; i < polygons.length; i++) { sideVertexCount += polygons[i].points.length / 3; sideTriangleCount += polygons[i].indices.length / 3; } var vertexCount = sideVertexCount * 2 + sideVertexCount * 4; var triangleCount = sideTriangleCount * 2 + sideVertexCount * 2; return { vertexCount: vertexCount, triangleCount: triangleCount }; }, _updatePolygonGeometry: function (componentModel, geometry, dataIndex, regionHeight, vertexOffset, triangleOffset, color) { // FIXME var projectUVOnGround = componentModel.get('projectUVOnGround'); var positionAttr = geometry.attributes.position; var normalAttr = geometry.attributes.normal; var texcoordAttr = geometry.attributes.texcoord0; var colorAttr = geometry.attributes.color; var polygons = this._triangulationResults[dataIndex - this._startIndex]; var hasColor = colorAttr.value && color; var indices = geometry.indices; var extrudeCoordIndex = this.extrudeY ? 1 : 2; var sideCoordIndex = this.extrudeY ? 2 : 1; var scale = [this.rootNode.worldTransform.x.len(), this.rootNode.worldTransform.y.len(), this.rootNode.worldTransform.z.len()]; var min = vec3.mul([], this._geoBoundingBox[0], scale); var max = vec3.mul([], this._geoBoundingBox[1], scale); var maxDimSize = Math.max(max[0] - min[0], max[2] - min[2]); function addVertices(polygon, y, insideOffset) { var points = polygon.points; var pointsLen = points.length; var currentPosition = []; var uv = []; for (var k = 0; k < pointsLen; k += 3) { currentPosition[0] = points[k]; currentPosition[extrudeCoordIndex] = y; currentPosition[sideCoordIndex] = points[k + 2]; uv[0] = (points[k] * scale[0] - min[0]) / maxDimSize; uv[1] = (points[k + 2] * scale[sideCoordIndex] - min[2]) / maxDimSize; positionAttr.set(vertexOffset, currentPosition); if (hasColor) { colorAttr.set(vertexOffset, color); } texcoordAttr.set(vertexOffset++, uv); } } function buildTopBottom(polygon, y, insideOffset) { var startVertexOffset = vertexOffset; addVertices(polygon, y, insideOffset); var len = polygon.indices.length; for (var k = 0; k < len; k++) { indices[triangleOffset * 3 + k] = polygon.indices[k] + startVertexOffset; } triangleOffset += polygon.indices.length / 3; } var normalTop = this.extrudeY ? [0, 1, 0] : [0, 0, 1]; var normalBottom = vec3.negate([], normalTop); for (var p = 0; p < polygons.length; p++) { var startVertexOffset = vertexOffset; var polygon = polygons[p]; // BOTTOM buildTopBottom(polygon, 0, 0); // TOP buildTopBottom(polygon, regionHeight, 0); var ringVertexCount = polygon.points.length / 3; for (var v = 0; v < ringVertexCount; v++) { normalAttr.set(startVertexOffset + v, normalBottom); normalAttr.set(startVertexOffset + v + ringVertexCount, normalTop); } var quadToTriangle = [0, 3, 1, 1, 3, 2]; var quadPos = [[], [], [], []]; var a = []; var b = []; var normal = []; var uv = []; var len = 0; for (var v = 0; v < ringVertexCount; v++) { var next = (v + 1) % ringVertexCount; var dx = (polygon.points[next * 3] - polygon.points[v * 3]) * scale[0]; var dy = (polygon.points[next * 3 + 2] - polygon.points[v * 3 + 2]) * scale[sideCoordIndex]; var sideLen = Math.sqrt(dx * dx + dy * dy); // 0----1 // 3----2 for (var k = 0; k < 4; k++) { var isCurrent = k === 0 || k === 3; var idx3 = (isCurrent ? v : next) * 3; quadPos[k][0] = polygon.points[idx3]; quadPos[k][extrudeCoordIndex] = k > 1 ? regionHeight : 0; quadPos[k][sideCoordIndex] = polygon.points[idx3 + 2]; positionAttr.set(vertexOffset + k, quadPos[k]); if (projectUVOnGround) { uv[0] = (polygon.points[idx3] * scale[0] - min[0]) / maxDimSize; uv[1] = (polygon.points[idx3 + 2] * scale[sideCoordIndex] - min[sideCoordIndex]) / maxDimSize; } else { uv[0] = (isCurrent ? len : len + sideLen) / maxDimSize; uv[1] = (quadPos[k][extrudeCoordIndex] * scale[extrudeCoordIndex] - min[extrudeCoordIndex]) / maxDimSize; } texcoordAttr.set(vertexOffset + k, uv); } vec3.sub(a, quadPos[1], quadPos[0]); vec3.sub(b, quadPos[3], quadPos[0]); vec3.cross(normal, a, b); vec3.normalize(normal, normal); for (var k = 0; k < 4; k++) { normalAttr.set(vertexOffset + k, normal); if (hasColor) { colorAttr.set(vertexOffset + k, color); } } for (var k = 0; k < 6; k++) { indices[triangleOffset * 3 + k] = quadToTriangle[k] + vertexOffset; } vertexOffset += 4; triangleOffset += 2; len += sideLen; } } geometry.dirty(); return { vertexOffset: vertexOffset, triangleOffset: triangleOffset }; }, _getRegionLinesInfo: function (idx, componentModel, geometry) { var vertexCount = 0; var triangleCount = 0; var regionModel = componentModel.getRegionModel(idx); var itemStyleModel = regionModel.getModel('itemStyle'); var lineWidth = itemStyleModel.get('borderWidth'); if (lineWidth > 0) { var polygonCoords = componentModel.getRegionPolygonCoords(idx); polygonCoords.forEach(function (coords) { var exterior = coords.exterior; var interiors = coords.interiors; vertexCount += geometry.getPolylineVertexCount(exterior); triangleCount += geometry.getPolylineTriangleCount(exterior); for (var i = 0; i < interiors.length; i++) { vertexCount += geometry.getPolylineVertexCount(interiors[i]); triangleCount += geometry.getPolylineTriangleCount(interiors[i]); } }, this); } return { vertexCount: vertexCount, triangleCount: triangleCount }; }, _updateLinesGeometry: function (geometry, componentModel, dataIndex, regionHeight, lineWidth, transform) { function convertToPoints3(polygon) { var points = new Float64Array(polygon.length * 3); var offset = 0; var pos = []; for (var i = 0; i < polygon.length; i++) { pos[0] = polygon[i][0]; // Add a offset to avoid z-fighting pos[1] = regionHeight + 0.1; pos[2] = polygon[i][1]; if (transform) { vec3.transformMat4(pos, pos, transform); } points[offset++] = pos[0]; points[offset++] = pos[1]; points[offset++] = pos[2]; } return points; } var whiteColor = [1, 1, 1, 1]; var coords = componentModel.getRegionPolygonCoords(dataIndex); coords.forEach(function (geo) { var exterior = geo.exterior; var interiors = geo.interiors; geometry.addPolyline(convertToPoints3(exterior), whiteColor, lineWidth); for (var i = 0; i < interiors.length; i++) { geometry.addPolyline(convertToPoints3(interiors[i]), whiteColor, lineWidth); } }); }, highlight: function (dataIndex) { var data = this._data; if (!data) { return; } var itemModel = data.getItemModel(dataIndex); var emphasisItemStyleModel = itemModel.getModel(['emphasis', 'itemStyle']); var emphasisColor = emphasisItemStyleModel.get('color'); var emphasisOpacity = retrieve.firstNotNull(emphasisItemStyleModel.get('opacity'), getItemVisualOpacity(data, dataIndex), 1); if (emphasisColor == null) { var color = getItemVisualColor(data, dataIndex); emphasisColor = echarts.color.lift(color, -0.4); } if (emphasisOpacity == null) { emphasisOpacity = getItemVisualOpacity(data, dataIndex); } var colorArr = graphicGL.parseColor(emphasisColor); colorArr[3] *= emphasisOpacity; this._setColorOfDataIndex(data, dataIndex, colorArr); }, downplay: function (dataIndex) { var data = this._data; if (!data) { return; } var itemStyleModel = data.getItemModel(dataIndex); var color = retrieve.firstNotNull(getItemVisualColor(data, dataIndex), itemStyleModel.get(['itemStyle', 'color']), '#fff'); var opacity = retrieve.firstNotNull(getItemVisualOpacity(data, dataIndex), itemStyleModel.get(['itemStyle', 'opacity']), 1); var colorArr = graphicGL.parseColor(color); colorArr[3] *= opacity; this._setColorOfDataIndex(data, dataIndex, colorArr); }, dispose: function () { this._labelsBuilder.dispose(); }, _setColorOfDataIndex: function (data, dataIndex, colorArr) { if (dataIndex < this._startIndex && dataIndex > this._endIndex) { return; } dataIndex -= this._startIndex; for (var i = this._vertexRangeOfDataIndex[dataIndex * 2]; i < this._vertexRangeOfDataIndex[dataIndex * 2 + 1]; i++) { this._polygonMesh.geometry.attributes.color.set(i, colorArr); } this._polygonMesh.geometry.dirty(); this._api.getZr().refresh(); } }; export default Geo3DBuilder;