import Mesh from 'claygl/src/Mesh'; import Renderer from 'claygl/src/Renderer'; import Texture2D from 'claygl/src/Texture2D'; import Texture from 'claygl/src/Texture'; import Shader from 'claygl/src/Shader'; import Material from 'claygl/src/Material'; import Node3D from 'claygl/src/Node'; import Geometry from 'claygl/src/Geometry'; import * as echarts from 'echarts/lib/echarts'; import Scene from 'claygl/src/Scene'; import LRUCache from 'zrender/lib/core/LRU'; import textureUtil from 'claygl/src/util/texture'; import EChartsSurface from './EChartsSurface'; import AmbientCubemapLight from 'claygl/src/light/AmbientCubemap'; import AmbientSHLight from 'claygl/src/light/AmbientSH'; import shUtil from 'claygl/src/util/sh'; import retrieve from './retrieve'; import SphereGeometry from 'claygl/src/geometry/Sphere'; import PlaneGeometry from 'claygl/src/geometry/Plane'; import CubeGeometry from 'claygl/src/geometry/Cube'; import AmbientLight from 'claygl/src/light/Ambient'; import DirectionalLight from 'claygl/src/light/Directional'; import PointLight from 'claygl/src/light/Point'; import SpotLight from 'claygl/src/light/Spot'; import PerspectiveCamera from 'claygl/src/camera/Perspective'; import OrthographicCamera from 'claygl/src/camera/Orthographic'; // Math import Vector2 from 'claygl/src/math/Vector2'; import Vector3 from 'claygl/src/math/Vector3'; import Vector4 from 'claygl/src/math/Vector4'; import Quaternion from 'claygl/src/math/Quaternion'; import Matrix2 from 'claygl/src/math/Matrix2'; import Matrix2d from 'claygl/src/math/Matrix2d'; import Matrix3 from 'claygl/src/math/Matrix3'; import Matrix4 from 'claygl/src/math/Matrix4'; import Plane from 'claygl/src/math/Plane'; import Ray from 'claygl/src/math/Ray'; import BoundingBox from 'claygl/src/math/BoundingBox'; import Frustum from 'claygl/src/math/Frustum'; import animatableMixin from './animatableMixin'; // Some common shaders import utilGLSL from 'claygl/src/shader/source/util.glsl.js'; import prezGLSL from 'claygl/src/shader/source/prez.glsl.js'; import commonGLSL from './shader/common.glsl.js'; import colorGLSL from './shader/color.glsl.js'; import lambertGLSL from './shader/lambert.glsl.js'; import realisticGLSL from './shader/realistic.glsl.js'; import hatchingGLSL from './shader/hatching.glsl.js'; import shadowGLSL from './shader/shadow.glsl.js'; Object.assign(Node3D.prototype, animatableMixin); Shader.import(utilGLSL); Shader.import(prezGLSL); Shader.import(commonGLSL); Shader.import(colorGLSL); Shader.import(lambertGLSL); Shader.import(realisticGLSL); Shader.import(hatchingGLSL); Shader.import(shadowGLSL); function isValueNone(value) { return !value || value === 'none'; } function isValueImage(value) { return value instanceof HTMLCanvasElement || value instanceof HTMLImageElement || value instanceof Image; } function isECharts(value) { return value.getZr && value.setOption; } // Overwrite addToScene and removeFromScene var oldAddToScene = Scene.prototype.addToScene; var oldRemoveFromScene = Scene.prototype.removeFromScene; Scene.prototype.addToScene = function (node) { oldAddToScene.call(this, node); if (this.__zr) { var zr = this.__zr; node.traverse(function (child) { child.__zr = zr; if (child.addAnimatorsToZr) { child.addAnimatorsToZr(zr); } }); } }; Scene.prototype.removeFromScene = function (node) { oldRemoveFromScene.call(this, node); node.traverse(function (child) { var zr = child.__zr; child.__zr = null; if (zr && child.removeAnimatorsFromZr) { child.removeAnimatorsFromZr(zr); } }); }; /** * @param {string} textureName * @param {string|HTMLImageElement|HTMLCanvasElement} imgValue * @param {module:echarts/ExtensionAPI} api * @param {Object} [textureOpts] */ Material.prototype.setTextureImage = function (textureName, imgValue, api, textureOpts) { if (!this.shader) { return; } var zr = api.getZr(); var material = this; var texture; material.autoUpdateTextureStatus = false; // disableTexture first material.disableTexture(textureName); if (!isValueNone(imgValue)) { texture = graphicGL.loadTexture(imgValue, api, textureOpts, function (texture) { material.enableTexture(textureName); zr && zr.refresh(); }); // Set texture immediately for other code to verify if have this texture. material.set(textureName, texture); } return texture; }; var graphicGL = {}; graphicGL.Renderer = Renderer; graphicGL.Node = Node3D; graphicGL.Mesh = Mesh; graphicGL.Shader = Shader; graphicGL.Material = Material; graphicGL.Texture = Texture; graphicGL.Texture2D = Texture2D; // Geometries graphicGL.Geometry = Geometry; graphicGL.SphereGeometry = SphereGeometry; graphicGL.PlaneGeometry = PlaneGeometry; graphicGL.CubeGeometry = CubeGeometry; // Lights graphicGL.AmbientLight = AmbientLight; graphicGL.DirectionalLight = DirectionalLight; graphicGL.PointLight = PointLight; graphicGL.SpotLight = SpotLight; // Cameras graphicGL.PerspectiveCamera = PerspectiveCamera; graphicGL.OrthographicCamera = OrthographicCamera; // Math graphicGL.Vector2 = Vector2; graphicGL.Vector3 = Vector3; graphicGL.Vector4 = Vector4; graphicGL.Quaternion = Quaternion; graphicGL.Matrix2 = Matrix2; graphicGL.Matrix2d = Matrix2d; graphicGL.Matrix3 = Matrix3; graphicGL.Matrix4 = Matrix4; graphicGL.Plane = Plane; graphicGL.Ray = Ray; graphicGL.BoundingBox = BoundingBox; graphicGL.Frustum = Frustum; // Texture utilities var blankImage = null; function getBlankImage() { if (blankImage !== null) { return blankImage; } blankImage = textureUtil.createBlank('rgba(255,255,255,0)').image; return blankImage; } function nearestPowerOfTwo(val) { return Math.pow(2, Math.round(Math.log(val) / Math.LN2)); } function convertTextureToPowerOfTwo(texture) { if ((texture.wrapS === Texture.REPEAT || texture.wrapT === Texture.REPEAT) && texture.image) { // var canvas = document.createElement('canvas'); var width = nearestPowerOfTwo(texture.width); var height = nearestPowerOfTwo(texture.height); if (width !== texture.width || height !== texture.height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var ctx = canvas.getContext('2d'); ctx.drawImage(texture.image, 0, 0, width, height); texture.image = canvas; } } } /** * @param {string|HTMLImageElement|HTMLCanvasElement} imgValue * @param {module:echarts/ExtensionAPI} api * @param {Object} [textureOpts] * @param {Function} cb */ // TODO Promise, test graphicGL.loadTexture = function (imgValue, api, textureOpts, cb) { if (typeof textureOpts === 'function') { cb = textureOpts; textureOpts = {}; } textureOpts = textureOpts || {}; var keys = Object.keys(textureOpts).sort(); var prefix = ''; for (var i = 0; i < keys.length; i++) { prefix += keys[i] + '_' + textureOpts[keys[i]] + '_'; } var textureCache = api.__textureCache = api.__textureCache || new LRUCache(20); if (isECharts(imgValue)) { var id = imgValue.__textureid__; var textureObj = textureCache.get(prefix + id); if (!textureObj) { var surface = new EChartsSurface(imgValue); surface.onupdate = function () { api.getZr().refresh(); }; textureObj = { texture: surface.getTexture() }; for (var i = 0; i < keys.length; i++) { textureObj.texture[keys[i]] = textureOpts[keys[i]]; } id = imgValue.__textureid__ || '__ecgl_ec__' + textureObj.texture.__uid__; imgValue.__textureid__ = id; textureCache.put(prefix + id, textureObj); cb && cb(textureObj.texture); } else { textureObj.texture.surface.setECharts(imgValue); cb && cb(textureObj.texture); } return textureObj.texture; } else if (isValueImage(imgValue)) { var id = imgValue.__textureid__; var textureObj = textureCache.get(prefix + id); if (!textureObj) { textureObj = { texture: new graphicGL.Texture2D({ image: imgValue }) }; for (var i = 0; i < keys.length; i++) { textureObj.texture[keys[i]] = textureOpts[keys[i]]; } id = imgValue.__textureid__ || '__ecgl_image__' + textureObj.texture.__uid__; imgValue.__textureid__ = id; textureCache.put(prefix + id, textureObj); convertTextureToPowerOfTwo(textureObj.texture); // TODO Next tick? cb && cb(textureObj.texture); } return textureObj.texture; } else { var textureObj = textureCache.get(prefix + imgValue); if (textureObj) { if (textureObj.callbacks) { // Add to pending callbacks textureObj.callbacks.push(cb); } else { // TODO Next tick? cb && cb(textureObj.texture); } } else { // Maybe base64 if (imgValue.match(/.hdr$|^data:application\/octet-stream/)) { textureObj = { callbacks: [cb] }; var texture = textureUtil.loadTexture(imgValue, { exposure: textureOpts.exposure, fileType: 'hdr' }, function () { texture.dirty(); textureObj.callbacks.forEach(function (cb) { cb && cb(texture); }); textureObj.callbacks = null; }); textureObj.texture = texture; textureCache.put(prefix + imgValue, textureObj); } else { var texture = new graphicGL.Texture2D({ image: new Image() }); for (var i = 0; i < keys.length; i++) { texture[keys[i]] = textureOpts[keys[i]]; } textureObj = { texture: texture, callbacks: [cb] }; var originalImage = texture.image; originalImage.onload = function () { texture.image = originalImage; convertTextureToPowerOfTwo(texture); texture.dirty(); textureObj.callbacks.forEach(function (cb) { cb && cb(texture); }); textureObj.callbacks = null; }; originalImage.crossOrigin = 'Anonymous'; originalImage.src = imgValue; // Use blank image as place holder. texture.image = getBlankImage(); textureCache.put(prefix + imgValue, textureObj); } } return textureObj.texture; } }; /** * Create ambientCubemap and ambientSH light. respectively to have specular and diffuse light * @return {Object} { specular, diffuse } */ graphicGL.createAmbientCubemap = function (opt, renderer, api, cb) { opt = opt || {}; var textureUrl = opt.texture; var exposure = retrieve.firstNotNull(opt.exposure, 1.0); var ambientCubemap = new AmbientCubemapLight({ intensity: retrieve.firstNotNull(opt.specularIntensity, 1.0) }); var ambientSH = new AmbientSHLight({ intensity: retrieve.firstNotNull(opt.diffuseIntensity, 1.0), coefficients: [0.844, 0.712, 0.691, -0.037, 0.083, 0.167, 0.343, 0.288, 0.299, -0.041, -0.021, -0.009, -0.003, -0.041, -0.064, -0.011, -0.007, -0.004, -0.031, 0.034, 0.081, -0.060, -0.049, -0.060, 0.046, 0.056, 0.050] }); ambientCubemap.cubemap = graphicGL.loadTexture(textureUrl, api, { exposure: exposure }, function () { // TODO Performance when multiple view ambientCubemap.cubemap.flipY = false; if (process.env.NODE_ENV !== 'production') { var time = Date.now(); } ambientCubemap.prefilter(renderer, 32); if (process.env.NODE_ENV !== 'production') { var dTime = Date.now() - time; console.log('Prefilter environment map: ' + dTime + 'ms'); } ambientSH.coefficients = shUtil.projectEnvironmentMap(renderer, ambientCubemap.cubemap, { lod: 1 }); cb && cb(); // TODO Refresh ? }); return { specular: ambientCubemap, diffuse: ambientSH }; }; /** * Create a blank texture for placeholder */ graphicGL.createBlankTexture = textureUtil.createBlank; /** * If value is image * @param {*} * @return {boolean} */ graphicGL.isImage = isValueImage; graphicGL.additiveBlend = function (gl) { gl.blendEquation(gl.FUNC_ADD); gl.blendFunc(gl.SRC_ALPHA, gl.ONE); }; /** * @param {string|Array.} colorStr * @param {Array.} [rgba] * @return {Array.} rgba */ graphicGL.parseColor = function (colorStr, rgba) { if (colorStr instanceof Array) { if (!rgba) { rgba = []; } // Color has been parsed. rgba[0] = colorStr[0]; rgba[1] = colorStr[1]; rgba[2] = colorStr[2]; if (colorStr.length > 3) { rgba[3] = colorStr[3]; } else { rgba[3] = 1; } return rgba; } rgba = echarts.color.parse(colorStr || '#000', rgba) || [0, 0, 0, 0]; rgba[0] /= 255; rgba[1] /= 255; rgba[2] /= 255; return rgba; }; /** * Convert alpha beta rotation to direction. * @param {number} alpha * @param {number} beta * @return {Array.} */ graphicGL.directionFromAlphaBeta = function (alpha, beta) { var theta = alpha / 180 * Math.PI + Math.PI / 2; var phi = -beta / 180 * Math.PI + Math.PI / 2; var dir = []; var r = Math.sin(theta); dir[0] = r * Math.cos(phi); dir[1] = -Math.cos(theta); dir[2] = r * Math.sin(phi); return dir; }; /** * Get shadow resolution from shadowQuality configuration */ graphicGL.getShadowResolution = function (shadowQuality) { var shadowResolution = 1024; switch (shadowQuality) { case 'low': shadowResolution = 512; break; case 'medium': break; case 'high': shadowResolution = 2048; break; case 'ultra': shadowResolution = 4096; break; } return shadowResolution; }; /** * Shading utilities */ graphicGL.COMMON_SHADERS = ['lambert', 'color', 'realistic', 'hatching', 'shadow']; /** * Create shader including vertex and fragment * @param {string} prefix. */ graphicGL.createShader = function (prefix) { if (prefix === 'ecgl.shadow') { prefix = 'ecgl.displayShadow'; } var vertexShaderStr = Shader.source(prefix + '.vertex'); var fragmentShaderStr = Shader.source(prefix + '.fragment'); if (!vertexShaderStr) { console.error('Vertex shader of \'%s\' not exits', prefix); } if (!fragmentShaderStr) { console.error('Fragment shader of \'%s\' not exits', prefix); } var shader = new Shader(vertexShaderStr, fragmentShaderStr); shader.name = prefix; return shader; }; graphicGL.createMaterial = function (prefix, defines) { if (!(defines instanceof Array)) { defines = [defines]; } var shader = graphicGL.createShader(prefix); var material = new Material({ shader: shader }); defines.forEach(function (defineName) { if (typeof defineName === 'string') { material.define(defineName); } }); return material; }; /** * Set material from model. * @param {clay.Material} material * @param {module:echarts/model/Model} model * @param {module:echarts/ExtensionAPI} api */ graphicGL.setMaterialFromModel = function (shading, material, model, api) { material.autoUpdateTextureStatus = false; var materialModel = model.getModel(shading + 'Material'); var detailTexture = materialModel.get('detailTexture'); var uvRepeat = retrieve.firstNotNull(materialModel.get('textureTiling'), 1.0); var uvOffset = retrieve.firstNotNull(materialModel.get('textureOffset'), 0.0); if (typeof uvRepeat === 'number') { uvRepeat = [uvRepeat, uvRepeat]; } if (typeof uvOffset === 'number') { uvOffset = [uvOffset, uvOffset]; } var repeatParam = uvRepeat[0] > 1 || uvRepeat[1] > 1 ? graphicGL.Texture.REPEAT : graphicGL.Texture.CLAMP_TO_EDGE; var textureOpt = { anisotropic: 8, wrapS: repeatParam, wrapT: repeatParam }; if (shading === 'realistic') { var roughness = materialModel.get('roughness'); var metalness = materialModel.get('metalness'); if (metalness != null) { // Try to treat as a texture, TODO More check if (isNaN(metalness)) { material.setTextureImage('metalnessMap', metalness, api, textureOpt); metalness = retrieve.firstNotNull(materialModel.get('metalnessAdjust'), 0.5); } } else { // Default metalness. metalness = 0; } if (roughness != null) { // Try to treat as a texture, TODO More check if (isNaN(roughness)) { material.setTextureImage('roughnessMap', roughness, api, textureOpt); roughness = retrieve.firstNotNull(materialModel.get('roughnessAdjust'), 0.5); } } else { // Default roughness. roughness = 0.5; } var normalTextureVal = materialModel.get('normalTexture'); material.setTextureImage('detailMap', detailTexture, api, textureOpt); material.setTextureImage('normalMap', normalTextureVal, api, textureOpt); material.set({ roughness: roughness, metalness: metalness, detailUvRepeat: uvRepeat, detailUvOffset: uvOffset }); // var normalTexture = material.get('normalMap'); // if (normalTexture) { // PENDING // normalTexture.format = Texture.SRGB; // } } else if (shading === 'lambert') { material.setTextureImage('detailMap', detailTexture, api, textureOpt); material.set({ detailUvRepeat: uvRepeat, detailUvOffset: uvOffset }); } else if (shading === 'color') { material.setTextureImage('detailMap', detailTexture, api, textureOpt); material.set({ detailUvRepeat: uvRepeat, detailUvOffset: uvOffset }); } else if (shading === 'hatching') { var tams = materialModel.get('hatchingTextures') || []; if (tams.length < 6) { if (process.env.NODE_ENV !== 'production') { console.error('Invalid hatchingTextures.'); } } for (var i = 0; i < 6; i++) { material.setTextureImage('hatch' + (i + 1), tams[i], api, { anisotropic: 8, wrapS: graphicGL.Texture.REPEAT, wrapT: graphicGL.Texture.REPEAT }); } material.set({ detailUvRepeat: uvRepeat, detailUvOffset: uvOffset }); } }; graphicGL.updateVertexAnimation = function (mappingAttributes, previousMesh, currentMesh, seriesModel) { var enableAnimation = seriesModel.get('animation'); var duration = seriesModel.get('animationDurationUpdate'); var easing = seriesModel.get('animationEasingUpdate'); var shadowDepthMaterial = currentMesh.shadowDepthMaterial; if (enableAnimation && previousMesh && duration > 0 // Only animate when bar count are not changed && previousMesh.geometry.vertexCount === currentMesh.geometry.vertexCount) { currentMesh.material.define('vertex', 'VERTEX_ANIMATION'); currentMesh.ignorePreZ = true; if (shadowDepthMaterial) { shadowDepthMaterial.define('vertex', 'VERTEX_ANIMATION'); } for (var i = 0; i < mappingAttributes.length; i++) { currentMesh.geometry.attributes[mappingAttributes[i][0]].value = previousMesh.geometry.attributes[mappingAttributes[i][1]].value; } currentMesh.geometry.dirty(); currentMesh.__percent = 0; currentMesh.material.set('percent', 0); currentMesh.stopAnimation(); currentMesh.animate().when(duration, { __percent: 1 }).during(function () { currentMesh.material.set('percent', currentMesh.__percent); if (shadowDepthMaterial) { shadowDepthMaterial.set('percent', currentMesh.__percent); } }).done(function () { currentMesh.ignorePreZ = false; currentMesh.material.undefine('vertex', 'VERTEX_ANIMATION'); if (shadowDepthMaterial) { shadowDepthMaterial.undefine('vertex', 'VERTEX_ANIMATION'); } }).start(easing); } else { currentMesh.material.undefine('vertex', 'VERTEX_ANIMATION'); if (shadowDepthMaterial) { shadowDepthMaterial.undefine('vertex', 'VERTEX_ANIMATION'); } } }; export default graphicGL;