Source: lib/sys/thrender.js


import * as THREE from 'three'
import {EffectComposer} from '../../packages/three/postprocessing/EffectComposer'
import {RenderPass} from  '../../packages/three/postprocessing/RenderPass'
import {OutlinePass} from '../../packages/three/postprocessing/OutlinePass'
import {Reflector} from '../../packages/three/core/Reflector'

import * as ECS from '../../packages/ecs-js/index'

import {XError, ramTexture} from '../xutils/xcommon'
import AssetKeepr from '../xutils/assetkeepr'
import {x} from '../xapp/xworld'
import {AssetType, ShaderFlag} from '../component/visual'
import {Obj3Type} from '../component/obj3'
import * as xutils from '../xutils/xcommon'
import * as xglsl from '../xutils/xglsl'
import GlUniform from '../xutils/gluniform'
import {vec3, mat4} from '../xmath/vec'
import xmath from '../xmath/math'
import xgeom from '../xmath/geom'

const any = ['Obj3'];

/**@class Thrender
 * @classdesc
 * X renderer of ecs subsystem based on Three.js renderer.
 */
export default class Thrender extends ECS.System {
	/**
	 * @param {ECS} ecs
	 * @param {x} x {options, ...}
	 * @constructor Thrender */
	constructor(ecs, x) {
		super(ecs);
		x.xthrender = this;
		this.ecs = ecs;
		var container = x.container;
		var scn;
		if (typeof container === 'object') {
			var resltx = this.init(container, x)
			scn = resltx.scene;
			this.light = resltx.light; // TODO probably we need another system
			x.light = this.light.options;
		}
		else {
			if (x.log >= 5)
				console.warn('[5] Sys.Thrender: container canvas is incorrect, renderer not initialized. Testing? ')
			else throw new XError('Thrender(): To initialize Thrender, a renderer must been created.');
		}

		// init Obj3 objects ( should be covered by mocha )
		// Business entities like created by Cubesys hasn't been visualized here yet,
		// but will present later when updating.
		var obj3Ents = ecs.queryEntities({any: any, hasnt: ['HudGroup', 'HudChild']});
		Thrender.createObj3s(ecs, scn, this.light.options, obj3Ents);
	}

	/**Create three.js Object3D
	 *
	 * - create mesh with geometry of Obj3.geom (e.g Obj3Type.BOX).
	 * @param {ECS} ecs
	 * @param {THREE.Scene} scene
	 * @param {THREE.Light} light
	 * @param {Array.<Entity>} entities
	 * @member Thrender.createObj3s
	 * @function
	 */
	static createObj3s(ecs, scene, light, entities) {
		// var mes = [];
		entities.forEach(function(e, x) {
			// A typicall orghogonal handling process should be blocks one after another,
			// like material, an generalized case of alp
			let tex, mat;
			let shadowSide = null;

			// 1. uniforms
			// 1.1 paras for uniforms
			var uniforms = new Object();
			// 1.2 shader's uniforms
			if ( ! e.Visual.material && e.Visual.shader
				// reflectex is a special material + shader ported from Three.js example,
				// Visaul.shader can be ignored
				|| e.Visual.vtype === AssetType.reflectex
				|| e.Visual.vtype === AssetType.reflector3js ) {
				uniforms = GlUniform.init(e.Visual, e.Obj3, light);
			}
			// 2. TODO merge material handling

			// common paras section
			// FIXME alp is deprcated by xglsl.formatUniforms()
			// - in fact, uniforms should been handled here
			var alp = e.Visual.paras !== undefined && e.Visual.paras.tex_alpha !== undefined ?
				e.Visual.paras.tex_alpha : 1;

			// tolerate API design error - Visual.asset will be deprecated
			if ((!e.Visual.paras || !e.Visual.paras.u_tex) && e.Visual.asset)
				e.Visual.paras = Object.assign(e.Visual.paras || {}, {u_tex: [e.Visual.asset]});

			if (e.Visual.paras && e.Visual.paras.shadowSide !== undefined)
				shadowSide = e.Visual.paras.shadowSide;

			if (!e.Obj3.datum)
				e.Obj3.datum = new Object();

			// create mesh of AssetType
			switch (e.Visual.vtype) {
				case AssetType.UserMesh:
					// nothing to do
					addObj3(scene, e.Obj3.mesh, e);
					break;
				case AssetType.Extension:
					// some extesion like Canvtex (x-visual v0.3) will handle this
					var g = new THREE.Group();
					// e.Obj3.mesh = g;
					addObj3(scene, g, e);
					break;
				case AssetType.gltf:
					if (!e.Visual.asset)
						throw XError('Thrender: AssetType.gltf needs Visual.asset parameter.');
					else if (e.Visual.paras && e.Visual.paras.nodes) {
						var nds = e.Visual.paras.nodes;
						if (typeof nds === 'string')
							nds = [nds]
						AssetKeepr.loadGltfNodes(e.Obj3, `assets/${e.Visual.asset}`,
							nds,
							(nodes) => {
								// Too late to push mesh into mes now, add to scene directly
								if (scene && nodes) {
									for (var n of nodes) {
										if (e.Obj3 && e.Obj3.transform) {
											var m4 = new mat4();

											if (e.Visual.paras && e.Visual.paras.withTransform) {
												// Debug Notes:
												// Looks like node's matrix is not the same Matrix in THREE.Matrix used by mat4.
												// n.matrix instanceof THREE.Matrix4 == false
												// But why?
												// 2020.05.17: Is this the asynchronous issue?
												if (typeof n.updateMatrix === 'function')
													n.updateMatrix();
												m4.setByjs(n.matrix);
											}

											for (var trs of e.Obj3.transform)
												m4.appAffine(trs);

											n.matrixAutoUpdate = false;
											m4.put2js(n.matrix);
										}
										e.Obj3.mesh = n;
										scene.add(n);
									}
								}
							});
					}
					else
						AssetKeepr.loadGltf(scene, e.Obj3, `assets/${e.Visual.asset}`);
					break;
				case AssetType.DomCanvas:
					// create texture of canvas
					AssetKeepr.loadCanvtex2(e.Canvas, e.Canvas.options);
					tex = e.Canvas.tex; // should be undefined now

					var side = !e.Visual.paras || e.Visual.paras.side === undefined
								? THREE.DoubleSide : e.Visual.paras.side;

					mat = new THREE.MeshBasicMaterial({
							opacity: alp,
							// debug note!!! This map must created here for later changing
							// map: new THREE.CanvasTexture(document.getElementById(e.Canvas.domId)),
							map: new ramTexture(3, 4, {alpha: 0.5}),

							transparent: true,
							side,
							shadowSide,
						});
					var m = Thrender.createObj3mesh(e.Obj3, e.Obj3.geom, mat);
					// can not been used for rendering until html canvas texture is ready,
					// when Canvas.dirty = true
					m.visible = false;
					addObj3(scene, m, e);
					break;
				case AssetType.mesh:
				case AssetType.mesh_basic:
					if (!e.Visual.material) {
						// FIXME material should handled in orthogonal fashion.
						if (e.Visual.shader) {
							// create ShaderMaterial
							mat = Thrender.createXShaderMaterial(e.Visual.shader, uniforms, e.Visual.paras, e.Obj3);
						}
						else {
							var def = {};
							if (e.Visual.asset)
								def.map = AssetKeepr.loadTexure( e.Visual.asset, function() {
									let def_ = def;
									return (texture) => {
										texture.needsUpdate = true;
										def_.map = texture;
									}
								}() );
							else // in case of test and default
								def.map = new ramTexture(3, 4, {alpha: alp});

							var side = !e.Visual.paras || e.Visual.paras.side === undefined
										? THREE.DoubleSide : e.Visual.paras.side;
							var alphaMap = e.Visual.paras && e.Visual.paras.enableAlphaMap ?
										def.map : undefined;
							var blending = e.Visual.paras && e.Visual.paras.blending !== undefined ?
										e.Visual.paras.blending : undefined;

							def = Object.assign( def, {
									// map: tex,
									opacity: alp,
									transparent: true,
									side,
									shadowSide,
									depthWrite: true,
									depthTest: true } );

							if ( alphaMap ) def.alphaMap = alphaMap;
							if ( blending !== undefined ) def.blending = blending;

							if (e.Visual.paras && e.Visual.paras.color)
								Object.assign (def, {color: e.Visual.paras.color});

							if (e.Visual.vtype === AssetType.mesh)
								mat = new THREE.MeshPhongMaterial(def);
							else // mesh_basic
								mat = new THREE.MeshBasicMaterial(def);
							mat.uniforms = Object.assign(mat.uniforms || new Object(), uniforms);
						}
					}

					var m = Thrender.createObj3mesh(e.Obj3, e.Obj3.geom, mat, e.Visual);
					addObj3(scene, m, e);
					break;
				case AssetType.wireframe:
					var def = {
							opacity: alp,
							transparent: true,
							wireframe: true,
							depthWrite: true,
							depthTest: true,// - for transparent
					};

					if (e.Visual.paras && e.Visual.paras.color)
						Object.assign (def, {color: e.Visual.paras.color});

					mat = e.Visual.material || new THREE.MeshBasicMaterial(def);
					Object.assign(mat, uniforms);

					var m = Thrender.createObj3mesh(e.Obj3, e.Obj3.geom, mat);
					addObj3(scene, m, e);
					break;
				case AssetType.point:
					// use Obj3.geom vertices as points
					if (!e.Visual.asset) {
						var modp = e.Visual.paras;
						mat = e.Visual.material
							|| new THREE.ShaderMaterial( { uniforms: GlUniform.obj2uniforms(uniforms) } );
						var mesh = Thrender.createObj3points(e.Obj3,
								e.Obj3.geom ? e.Obj3.geom : Obj3Type.SPHERE, mat);

						addObj3(scene, mesh, e);
					}
					// load model as points
					else if (e.Visual.asset) {
						if (e.Visual.paras.nodes) {
							AssetKeepr.loadGltfNodes(e.Obj3, `assets/${e.Visual.asset}`,
								e.Visual.paras.nodes,
								(nodes) => {
									// change gltf vertices to points buffer
									var m = Thrender.combineGltfPoints(e.Obj3, nodes, e.Visual.paras);
									e.Obj3.mesh = Thrender.applyTransform(m, e.Obj3.transform);

									// Too late to push mesh into mes now, add to scene directly
									addObj3(scene, m, e);
								});
						}
						else
							AssetKeepr.loadGltf(scene, e.Obj3, `assets/${e.Visual.asset}`,
								(scenes) => {
									var nodes;
									if (scenes.scene) nodes = scenes.scene.children;
									if (!nodes) {
										console.error ('No nodes found in gltf assets', e.Visual.asset);
									}
									else {
										// change gltf vertices to points buffer
										var grp = Thrender.replaceMaterials(nodes, e.Visual.paras);
										e.Obj3.mesh = Thrender.applyTransform(grp, e.Obj3.transform);

										// Too late to push mesh into mes now, add to scene directly
										addObj3(scene, grp, e);
									}
								});
					}
					break;
				case AssetType.refPoint:
				case AssetType.cubeVoxel:
					var eref = ecs.getEntity(e.Visual.asset);
					if (!eref) {
						console.error('Thrender.createObj3s(): can\'t find referenced entity: ',
								e.Visual.asset);
					}
					else if (!eref.Obj3.mesh) {
						console.error('Thrender.createObj3s(): can\'t find referenced entity mesh: ',
								e, eref);
					}
					// xglsl create Obj3 particles
					else {
						var pointMesh;
						if (e.Visual.vtype === AssetType.refPoint)
							pointMesh = Thrender.createObj3particles(e.Obj3, eref.Obj3.mesh, e.Visual);
						else if (e.Visual.vtype === AssetType.cubeVoxel)
							pointMesh = Thrender.createCubeVoxel(e.Obj3, e.Visual);
						else throw XError("shouldn't reaching here!");
						e.Obj3.mesh = pointMesh;

						addObj3(scene, pointMesh, e);
					}
					break;
				case AssetType.GeomCurve:
					// var {curve, path} = Thrender.createCurve(e.Obj3, e.Visual.paras);
					// addObj3(scene, curve, e);
					mat = Thrender.xlineMaterial(uniforms, e.Visual.paras);
					var {curve, path} = Thrender.createObj3Line(e.Obj3, e.Visual.paras, mat);
					addObj3(scene, curve, e);
					e.Obj3.datum.path = path;
					break;
				case AssetType.DynaSects:
					// TODO: this branch shows the only difference of DynaSects with GeomCurve is geometry type.
					// var {group, points} = Thrender.createDynaSects(e.Obj3, e.Visual.paras);
					mat = Thrender.xlineMaterial(uniforms, e.Visual.paras);
					var {curve, points} = Thrender.createObj3Line(e.Obj3, e.Visual.paras, mat);
					e.Obj3.mesh = curve;
					if (!e.Obj3.datum)
						e.Obj3.datum = new Object();
					e.Obj3.datum.points = points;
					addObj3(scene, curve, e);
					break;
				case AssetType.PathTube:
					if (!e.Visual.material) {
						// FIXME material should handled in orthogonal fashion.
						// FIXME see also createObj3particles(), AssetType.mesh
						if (e.Visual.shader) {
							// create ShaderMaterail
							mat = Thrender.createXShaderMaterial(e.Visual.shader, uniforms, e.Visual.paras, e.Obj3);
						}
						else {
							var def = {};
							if (e.Visual.asset)
								def.map = AssetKeepr.loadTexure( e.Visual.asset, function() {
									let def_ = def;
									return (texture) => {
										texture.needsUpdate = true;
										def_.map = texture;
									}
								}() );
							else // in case of test and default
								def.map = new ramTexture(3, 4, {alpha: alp});
							var side = !e.Visual.paras || e.Visual.paras.side === undefined
										? THREE.DoubleSide : e.Visual.paras.side;
							var alphaMap = e.Visual.paras && e.Visual.paras.enableAlphaMap ?
										def.map : undefined;
							def = Object.assign(def, {
									// map: tex,
									alphaMap,
									opacity: alp,
									side,
									shadowSide,
									transparent: true,
									depthWrite: true,
									depthTest: true,
									blending: THREE.AdditiveBlending } );

							if (e.Visual.paras && e.Visual.paras.color)
								Object.assign (def, {color: e.Visual.paras.color});

							if (e.Visual.vtype === AssetType.mesh)
								mat = new THREE.MeshPhongMaterial(def);
							else // mesh_basic
								mat = new THREE.MeshBasicMaterial(def);
							mat.uniforms = Object.assign(mat.uniforms || new Object(), uniforms);
						}
					}
					if (e.Visual.paras && e.Visual.paras.nodes) {
						// TODO branch canvasys task: svg path
						// TODO branch canvasys task: svg path
						// TODO branch canvasys task: svg path
						// TODO branch canvasys task: svg path
						Thrender.svgPathTube(e.Obj3, e.Visual.paras, mat,
							(meshes, group, paths) => {
								if (e.ModelSeqs) {
									if (!e.Obj3.datum)
										e.Obj3.datum = new Object();
									e.Obj3.datum.paths = paths;
								}

								addObj3(scene, group, e);
							});
					}
					else {
						// path from Visual.points
						var {mesh, path} = Thrender.createPathTube(e.Obj3, e.Visual.paras, mat);
						if (!!e.Obj3.invisible) tube.visible = false;
						addObj3(scene, mesh, e);

						// Design MEMO
						// what's the better practice for this in ECS pattern?
						e.Obj3.datum.path = path;
					}
					break;
				case AssetType.Arrow:
					var dir = new THREE.Vector3( e.Visual.paras.dir[0],
								e.Visual.paras.dir[1], e.Visual.paras.dir[2]);
					var org = new THREE.Vector3( e.Visual.paras.origin[0],
								e.Visual.paras.origin[1], e.Visual.paras.origin[2]);
					/*
					ArrowHelper(dir : Vector3, origin : Vector3, length : Number, hex : Number, headLength : Number, headWidth : Number )
					dir -- direction from origin. Must be a unit vector.
					origin -- Point at which the arrow starts.
					length -- length of the arrow. Default is 1.
					hex -- hexadecimal value to define color. Default is 0xffff00.
					headLength -- The length of the head of the arrow. Default is 0.2 * length.
					headWidth -- The width of the head of the arrow. Default is 0.2 * headLength.
					*/
					var a = new THREE.ArrowHelper( dir, org,
								e.Visual.paras.length, e.Visual.paras.color,
							 	e.Visual.paras.headLength, e.Visual.paras.headWidth);
					e.Obj3.mesh = a;
					addObj3(scene, a, e);
					break;
				case AssetType.reflector3js:
					var g = Thrender.geometryCase(e.Obj3.geom, e.Obj3.box,
									e.Visual.paras, e.Obj3);
					var m = new Reflector(g);
					e.Obj3.mesh = m;
					addObj3(scene, m, e);
					break;
				case AssetType.reflectex:
					var g = Thrender.geometryCase(e.Obj3.geom, e.Obj3.box,
									e.Visual.paras, e.Obj3);
					var shader = xglsl.xvShader( ShaderFlag.reflectex, e.Visual.paras );
					var m = new Reflector( g, {
								shader,
								receiveShadow: e.Obj3.mesh.receiveShadow } );
					// otherwise WebGLRenderer.initMaterial won't set light uniforms
					m.material.lights = true;
					Object.assign(m.material.uniforms, uniforms);

					// var m = new Reflector(g);
					e.Obj3.mesh = m;
					addObj3(scene, m, e);
					break;
				default:
					console.error('TODO AssetType: ', e.Visual.vtype);
			}

			Thrender.applyTransform(e.Obj3.mesh, e.Obj3.transform);

			if (Array.isArray(e.Obj3.channels)) {
				// e.Obj3.mesh.layers = 1;
				for (var ch of e.Obj3.channels) {
					e.Obj3.mesh.layers.enable( ch );
				}
			}
		});

		function addObj3(scene, mesh, e) {
			if (scene) {
				scene.add(mesh);
				// if (e.Obj3 && !e.Obj3.mesh)
				// 	e.Obj3.mesh = mesh;
				if ( e.Obj3 && (!e.Obj3.mesh
					|| !e.Obj3.mesh.matrix && !e.Obj3.mesh.material) )
					e.Obj3.mesh = Object.assign(mesh, e.Obj3.mesh);

				if (e.Obj3.group) {
					const g = ecs.getEntity(e.Obj3.group);
					if (g.Obj3.mesh)
						g.Obj3.mesh.add(mesh);
					else {
						console.error('Can not find entity\'s parent group: ', e);
						console.error('x-visual can only add child to parent that already has been created.');
					}
				}
				else {
					if (mesh.type === 'Group')
						mesh.xscene = scene; // for convenience laterly
				}
			}
			else if (x.log >= 5) { // testing
				console.warn('[5] undefined scene with Obj3 meshes: ', mes.length);
			}
		}
	}

	/**Set affine transform to mesh's local matrix.
	 * @param {THREE.Mesh} mesh
	 * @param {array} transf transformations
	 * @return {THREE.Mesh} mesh
	 * @member Thrender.applyTransform
	 * @function
	 */
	static applyTransform (mesh, transf) {
		if (mesh && transf) {
			var m4 = new mat4();
			for (var trs of transf)
				m4.appAffine(trs);

			mesh.matrixAutoUpdate = false;

			m4.put2js(mesh.matrix);
		}
		return mesh;
	}

	/**
	 * @param {ShaderFlag} shaderflag
	 * @param {object} uniforms Mesh.material.uniforms
	 * @param {object} vparas visual paras
	 * @param {Obj3} obj3 Entity Obj3
	 * @return {THREE.ShaderMaterial} material for x-shaders
	 * @member Thrender.createXShaderMaterial
	 * @function
	 */
	static createXShaderMaterial( shaderflag, uniforms = new Object(), vparas, obj3 ) {
		if (shaderflag === ShaderFlag.colorArray) {
			if (vparas && vparas.u_texWeight !== undefined) {
				uniforms.u_texWeight = {value: vparas.u_texWeight};
			}

			if (vparas && vparas.colors && vparas.colors.length > 0) {
				var clrs = [];
				for (var i = 0; i < vparas.colors.length; i++) {
					clrs.push( new THREE.Vector3(...vparas.colors[i]) );
				}
				uniforms.u_colors = {value: clrs};
			}
			// TODO merge with XMaterial.init()
			if (vparas && vparas.u_tex && vparas.u_tex.length > 0) {
				var texs = [];
				for (var i = 0; i < vparas.u_tex.length; i++) {
					var tex = new ramTexture( i + 1, i + 1, { alpha: 0.5 } );
					// suppres RanderPass warning - but not working
					tex.needsUpdate = false;
					texs.push( tex );

					AssetKeepr.loadTexure( vparas.u_tex[i],
						( function() {
							var j = i;
							return (texture) => {
								texture.needsUpdate = true;
								texs[j] = texture;
							}
						} )() );
				}
				uniforms.u_tex = { value: texs };

				if (!vparas || !vparas.colors || vparas.colors.length === 0) {
					// no diffuse colors
					vparas.colors = [[0, 0, 0]];
					uniforms.u_texWeight = {value: 1};
				}
			}
		}

		if ( obj3.mesh && obj3.mesh.receiveShadow )
			vparas.dirShadow = true;
		let {vertexShader, fragmentShader} = xglsl.xvShader( shaderflag, vparas );

		let side = !vparas || vparas.side === undefined
					? THREE.FrontSide : vparas.side;

		// TODO should this been moved to GlUniform?
		if ( obj3.mesh && obj3.mesh.receiveShadow ) {
			// Three.js directional light and map co-orperate with a few special
			// materials' uniforms
			uniforms = Object.assign(uniforms, THREE.ShaderLib.shadow.uniforms);
			delete uniforms.vertexShader;
			delete uniforms.fragmentShader;
		}

		let mat = new THREE.ShaderMaterial( {
			uniforms,
			vertexShader,
			fragmentShader,
			depthTest: true,
			transparent: true,
			depthWrite: true,
			side,
			shadowSide: vparas && vparas.shadowSide !== undefined ?
				vparas.shadowSide : THREE.BackSide,
		} );

		// This makes renderer update object's shadow map
		// see WebGLRenderer.materialNeedsLights() called by initMaterial()
		if ( obj3.mesh && obj3.mesh.receiveShadow )
			mat.lights = true;

		if (shaderflag === ShaderFlag.discard)
			// Debug Notes: avoid error message:
			// glDrawElements "buffer format and fragment output variable type incompatible"
			// see
			// https://stackoverflow.com/questions/57535605/three-js-custom-shader-error-on-chrome-gl-invalid-operation-active-draw-buffers
			// https://github.com/mrdoob/three.js/issues/17805
			mat.colorWrite = false;
		return mat;
	}

	/**Create a line material either one of xv.ShaderFlag.colorLine or the THREE.
	 * LineDashedMaterial, depends on wither dashSize or gapSize presented in vparas.
	 *
	 * Three.js LineBasicMaterial opacity works but has issues. We tried and seems
	 * it's not transparent. See also <a href='https://discourse.threejs.org/t/change-opacity-using-fat-lines-example/5523/7'>
	 * discussion: Change opacity using fat lines example for line opacity</a>
	 * @param {object} uniforms
	 * @param {object} vparas
	 * @return {object} {curve, path, points}
	 * @member Thrender.xlineMaterial
	 * @function
	 */
	static xlineMaterial( uniforms = new Object(), vparas ) {

		if (vparas.dashSize !== undefined || vparas.dashSize !== undefined) {
			var color = 0;
			if (Array.isArray(vparas.color)) {
				color = new THREE.Color(...vparas.color);
			}
			else if (vparas.color !== undefined) {
				color = new THREE.Color(vparas.color);
			}
			else
				color = 0xffff00;

			return new THREE.LineDashedMaterial( {
				color,
				linewidth: vparas.linewidth,
				scale: vparas.scale,
				dashSize: vparas.dashSize,
				gapSize: vparas.gapSize,
			} );
		}
		// use xline to control u_alpha
		else {
			var {vertexShader, fragmentShader} = xglsl.xvShader(
						ShaderFlag.colorLine, vparas);

			// add vec3 u_color
			if (Array.isArray(vparas.color)) {
				uniforms.u_color = { value: new THREE.Vector4(...vparas.color) };
			}
			else if (vparas.color !== undefined) {
				var c = new THREE.Color(vparas.color);
				uniforms.u_color = { value: new THREE.Vector4(c.r, c.g, c.b, c.a) };
			}
			else
				uniforms.u_color = {value: new THREE.Vector4(1, 1, 0, 1) };

			uniforms.u_alpha = { value: 1 };
			// uniforms.opacity = { value: 1 };

			var matOpt = {	uniforms,
							vertexShader,
							fragmentShader,
							depthTest: true,
							transparent: true,
							depthWrite: true };

			if (vparas.linewidth !== undefined)
				matOpt.linewidth = vparas.linewidth;

			var mat = new THREE.ShaderMaterial( matOpt );

			return mat;
		}
	}

	/**
	 * Generate line, segments or curve, e.g Obj3.geom =
	 * RandomCurve, RandomSects, PointSects, CatmullRom, Spline from
	 * {@link XComponent.Obj3Type}.
	 *
	 * 1. use {@link xgeom.generateCurve} to generate points
	 * 2. create the THREE.Line or LineSegments,
	 * @param {Obj3} obj3
	 * @param {object} paras Visual.paras, @see {@link xgeom.generateCurve}
	 * @param {Material} material
	 * @return {object} {curve, path, points}
	 * @member Thrender.createObj3Line
	 * @function
	 */
	static createObj3Line( obj3, paras, material ) {
		var {geomCurve, points} = xgeom.generateCurve(obj3, paras);
		var line;
		// FIXME we can ask xgeom here to make sure how to create line buffer
		// or let xgeom handle all of this?
		if (obj3.geom === Obj3Type.PointSects) {
			line = new THREE.LineSegments( geomCurve, material );
		}
		else {
			line = new THREE.Line( geomCurve, material );
		}
		line.computeLineDistances();
		return {curve: line, path: points, points};
	}

	/**Create THREE.Points from cmpObj3.mesh.
	 *
	 * Difference to createObj3particles which createing vertices from referenced
	 * mesh, this method create points according to Obj3.geomType ({@link Obj3Type})
	 * with Obj3.box as constructor parameters.
	 * @param {Obj3} cmpObj3 Obj3 for the mesh
	 * @param {AssetType} geomType Visual.vtype
	 * @param {THREE.Material} material [optional] default is THREE.ShaderMaterail.
	 * @return {THREE.Mesh} mesh (already set in cmpObj3)
	 * @member Thrender.createObj3points
	 * @function
	 */
	static createObj3points(cmpObj3, geomType, material) {
		if (cmpObj3) {
			var g = Thrender.geometryCase(geomType, cmpObj3.box);
			var m = new THREE.Points( g,
				material || new THREE.ShaderMaterial( { color: 0x888888 } ));
			if (!!cmpObj3.invisible) m.visible = false;

			cmpObj3.mesh = m;
			return m;
		}
		else console.error(
			'createObj3points(): can\'t create Obj3 for undefined Obj3 component. type: ',
			geomType);
	}

	/**Create object mesh, put int *cmpObj3*.
	 * @param {Obj3} cmpObj3
	 * @param {Obj3Type} geomType
	 * @param {THREE.Materail} material
	 * @param {object} [visual] Visual with additional paras, such as points of [[x, y, z]] for creating path, road, polygon, etc.
	 * @member Thrender.createObj3mesh
	 * @function
	 */
	static createObj3mesh(cmpObj3, geomType, material, visual) {
		if (cmpObj3) {
			var g = Thrender.geometryCase(geomType, cmpObj3.box,
							visual ? visual.paras : undefined, cmpObj3);
			var m = new THREE.Mesh( g, material );
			if (!!cmpObj3.invisible) m.visible = false;

			// cmpObj3.mesh = m;
			return m;
		}
		else console.error(
			'createObj3mesh(): can\'t create Obj3 for undefined Obj3 component. type: ',
			geomType);
	}

	/**Create object mesh, put int *cmpObj3*.
	 * This method handle only AssetType.PathTube.
	 * @see Thrender.createObj3mesh
	 * @param {Obj3} cmpObj3 {geom: Obj3Type.GeomCurve (with future sects?)}
	 * @param {object} paras Visual.paras, @see xgeom.generateCurve
	 * @param {THREE.Materail} material
	 * @return {object} {tube, path}, where geomCurve is THREE.BufferGeometry,
	 * @member Thrender.createObj3mesh
	 * @function
	 */
	static createPathTube(cmpObj3, paras, material) {
		var {tube, path} = xgeom.generateDirTube(cmpObj3, paras);
		var m = new THREE.Mesh( tube, material );

		cmpObj3.mesh = m;
		return {mesh: m, path};
	}

	static svgPathTube(cmpObj3, paras, material, onload) {
		if (!paras.points && paras.nodes && paras.nodes.length > 0)
			// load points from assets (FIXME currently only SVG)
			AssetKeepr.loadSvgPath('assets/' + paras.asset, paras.nodes,
				(paths) => {
					var pathgrp = [];
					var grp = new THREE.Group();
					for (var p of paths) {
						paras.points = p;
						var {tube, path} = xgeom.generateDirTube(cmpObj3, paras);
						var m = new THREE.Mesh( tube, material );
						pathgrp.push(m);
						grp.add(m);
					}
					onload( {meshes: pathgrp, group: grp, paths} );
				});
	}

	/**<div id='api-geometryCase'>Create geometry for different cases.</div>
	 * @param {XComponent.Obj3Type} geomType Obj3Type (geom types)
	 * @param {array<number>} geomParaArr geometry parameters for the given type.
	 * See {@link XComponent.Obj3Type Obj3Type} for parameters details.
	 * @param {object} vpara Visual.para - shouldn't been here once the orthogonal way is redesigned
	 * @param {ModelSeqs} [modelSeqs] path will be saved in modelSeqs.cache
	 * - Design Memo: when geometry handling became a separate process, this parameters should replaced by component?
	 * @return {THREE.BufferGeometry} any subclass of three-js BufferGeometry.
	 * @member Thrender.geometryCase
	 * @function
	 */
	static geometryCase(geomType, geomParaArr, vparas, obj3) {
		var len = geomParaArr.length;

		var {x, y, z, u, v, w, s, t} = Object.assign({
			x: geomParaArr[0], y: geomParaArr[1],
			z: len > 2 ? geomParaArr[2] : 1,
			u: len > 3 ? geomParaArr[3] : 1, v: len > 4 ? geomParaArr[4] : 1,
		 	w: len > 5 ? geomParaArr[5] : 1,
		 	s: len > 6 ? geomParaArr[6] : 0, t: len > 7 ? geomParaArr[7] : 0
		});

		var g;
		if (geomType === Obj3Type.BOX) {
			g = new THREE.BoxBufferGeometry( x, y, z );
		}
		else if (geomType === Obj3Type.PLANE) {
			g = new THREE.PlaneBufferGeometry( x, y, z );
		}
		else if (geomType === Obj3Type.SPHERE) {
			g = new THREE.SphereBufferGeometry( x, y, z );
		}
		else if (geomType === Obj3Type.TORUS) {
			// g = new THREE.TorusBufferGeometry(10, 3, 16, 100); // x, y, z, u );
			z = len > 2 ? geomParaArr[2] : 8;
			u = len > 3 ? geomParaArr[3] : 6;
			v = len > 4 ? geomParaArr[4] : Math.PI * 2;
			// radius - Radius of the torus, from the center of the torus to the center of the tube. Default is 1.
			// tube — Radius of the tube. Default is 0.4.
			// radialSegments — Default is 8
			// tubularSegments — Default is 6.
			// arc — Central angle. Default is Math.PI * 2.
			g = new THREE.TorusBufferGeometry( x, y, z, u, v );
		}
		else if (geomType === Obj3Type.CONE) {
			g = new THREE.ConeBufferGeometry( x, y, z );
		}
		else if (geomType === Obj3Type.Cylinder) {
			// Why so many restrictiong?
			// Because visualized is essential to debug.
			u = Math.max(3, u);
			if (v === 0 || v === undefined) v = 1;
			if (w === 0) w = undefined;
			if (s === 0) s = undefined;
			if (t === 0) t = undefined;
			// radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength
			g = new THREE.CylinderBufferGeometry( x, y, z, u, v, w, s, t );
		}
		else if (geomType === Obj3Type.SHAPE) {
			var shape = new THREE.Shape();
			shape.moveTo( x, y );
			for (var i = 2; i < len; i += 2) {
				shape.bezierCurveTo( geomParaArr[i], geomParaArr[i + 1] );
			}
			g = new THREE.ShapeBufferGeometry( shape );
		}
		else if (geomType === Obj3Type.RING) {
			g = new THREE.RingBufferGeometry( x, y, z, u, v, w );
		}
		else if (geomType === Obj3Type.Lathe) {
			g = new THREE.LatheBufferGeometry( x, y, z, u );
		}
		else if (geomType === Obj3Type.MapXZRoad) {
			if (vparas && vparas.features) {
				var {geom, path} = xgeom.generateWayxz( vparas.features,
					  vparas ? vparas.y0 || geomParaArr[0] || 0 : geomParaArr[0] || 0,
					  vparas ? vparas.origin : undefined,
					  vparas ? vparas.geostyle : undefined );
				g = geom;
			}
			else if (vparas && vparas.uri) {
				// path0 is a place holder for creating the new geometry latter.
				// Design Notes: is place holder a solution of asynchronous loading?
				var path0 = AssetKeepr.geojsonPaths(vparas.uri,
					new Object(),
					(paths) => {
						if (obj3.mesh) {
							// only created mesh needing new vertices
							var {geom, path} = xgeom.generateWayxz( paths,
								 vparas ? vparas.y0 || geomParaArr[0] || 0 : geomParaArr[0] || 0,
								 vparas ? vparas.origin : undefined,
								 vparas ? vparas.geostyle : undefined );
							if (obj3.mesh.geometry)
								obj3.mesh.geometry.dispose();
							obj3.mesh.geometry = geom;
							obj3.mesh.geometry.verticesNeedUpdate = true;
						}
						obj3.datum.path = path;
						// will trigger animizer rebuild AnimType.U_PATHn_MORPH, reset by animizer
						obj3.datum.dirty = true;
					} );
				var {geom, path} = xgeom.generateWayxz( path0,
					  vparas ? vparas.y0 || geomParaArr[0] || 0 : geomParaArr[0] || 0,
					  vparas ? vparas.origin : undefined,
					  vparas ? vparas.geostyle : undefined );
				g = geom;
			}
			obj3.datum.path = path;
			obj3.datum.dirty = true;
		}
		else if (geomType === Obj3Type.Hexatile) {
			if (vparas && vparas.features) {
				var {geom, points} = AssetKeepr.geoHexaprism( vparas.features, {
						radius: vparas.geostyle ? vparas.geostyle.radius || 1 : 1,
						height: vparas.geostyle ? vparas.geostyle.height || 1 : 1,
						geoScale: vparas.geostyle ? vparas.geostyle.scale || 1 : 1,
						geoCentre: vparas.origin || [0, 0, 0],
						onGroup: vparas.geostyle ? vparas.geostyle.onGroup : undefined
					} );
				g = geom;
				obj3.datum.locations = points;
				obj3.datum.dirty = true;
			}
			else if (vparas && vparas.uri) {
				var {geom} = AssetKeepr.geoHexaprismAsync(vparas.uri,
					{	count: vparas.count || 1,
						radius: vparas.geostyle ? vparas.geostyle.radius || 1 : 1,
						height: vparas.geostyle ? vparas.geostyle.height || 1 : 1,
						heightName: vparas.heightName || 'height',
						geoScale: vparas.geostyle ? vparas.geostyle.scale || 1 : 1,
						geoCentre: vparas.origin || [0, 0, 0],
						onGroup: vparas.geostyle ? vparas.geostyle.onGroup : undefined
					},
					(geom, points, count) => {
						if (obj3.mesh) {
							// only created mesh needing new vertices
							if (obj3.mesh.geometry)
								obj3.mesh.geometry.dispose();
							obj3.mesh.geometry = geom;
							obj3.mesh.geometry.verticesNeedUpdate = true;
						}
						obj3.datum.locations = points;
						obj3.datum.dirty = true;
					} );
				g = geom;
			}
		}
		else if (geomType === Obj3Type.GeoPrism) {
			// synchrodous
			if (vparas && vparas.features) {
				var opts = xgeom.formatPrismOption(obj3, vparas);
				var {geom, points} = AssetKeepr.geoTexturePrism( vparas.features, opts );
				g = geom;
				obj3.datum.locations = points;
				obj3.datum.dirty = true;
			}
			// asynchronous
			else if (vparas && vparas.uri) {
				var opts = xgeom.formatPrismOption(obj3, vparas);
				opts.obj3 = obj3;
				var {geom} = AssetKeepr.geoPrismAsync(vparas.uri, opts,
					(geom, points, count, opts) => {
						var obj = opts.obj3;
						if (obj.mesh) {
							// only created mesh needing new vertices
							if (obj.mesh.geometry)
								obj.mesh.geometry.dispose();
							obj.mesh.geometry = geom;
							obj.mesh.geometry.verticesNeedUpdate = true;
						}
						obj.datum.locations = points;
						obj.datum.dirty = true;
					} );
				g = geom;
			}
		}
		else {
			if (y > 5) {
				console.warn('geometryCase(): maximum for IcosahedronGeometry detail is 5',
					`argument value ${y} is ignored.`);
				y = 5;
			}
			if (geomType === Obj3Type.Tetrahedron) {
				g = new THREE.TetrahedronBufferGeometry( x, y, z );
			}
			else if (geomType === Obj3Type.Dodecahedron) {
				g = new THREE.DodecahedronBufferGeometry( x, y );
			}
			else if (geomType === Obj3Type.Octahedron) {
				g = new THREE.OctahedronBufferGeometry( x, y );
			}
			else if (geomType === Obj3Type.Icosahedron) {
				g = new THREE.IcosahedronBufferGeometry( x, y );
			}
			else
				throw new XError('TODO GEOM Type: ', geomType);
		}
		return g;
	}

	/**Clone and combine gltf nodes' geometry[position] buffer - the modules vertices.
	 *
	 * This method can only handling Visual.vtype == AssetType.point, making
	 * points (vertices) can basically rendered.
	 *
	 * This is not enough. Animizer would added more components to animize points.
	 *
	 * @param {xv.Components.Obj3}
	 * @param {array} nodes nodes loaded from gltf.
	 * @param {paras} Visual.paras, define, size, color...
	 * @return {THREE.Points} points
	 * @member Thrender.combineGltfPoints
	 * @function
	 */
	static combineGltfPoints(cmpObj3, nodes, paras) {
		if (cmpObj3 && nodes) {
			/* for city/scene.gltf, paras.nodes = ['Tree-1-3'],
			 * nodes[0].children[0].type == 'Mesh',
			 * nodes[0].children[0].geometry is a BufferGeometry, with array of
			 * BufferAttributes as 'attributes'.
			 * nodes[0].children[0].geometry.attributes['position'] ==
				length: 2772
				dynamic: false
				name: ""
				array: Float32Array(2772) [135.61163330078125, 31.193208694458008, -2.098475694656372, …]
				itemSize: 3
				count: 924
				normalized: false
				usage: 35044
				updateRange: {offset: 0, count: -1}
				version: 0
			*/

			// get count first - seems no better way
			// see Three.js example - dynamic points
			var count = 0;
			for (const node of nodes) {
				if (node.type === 'Mesh') {
					var buffer = node.geometry.attributes[ 'position' ];
					count += buffer.array.length;
				}
				for (const child of node.children) {
					if (child.type === 'Mesh') {
						var buffer = child.geometry.attributes[ 'position' ];
						count += buffer.array.length;
					}
				}
			}

			var combined = new Float32Array( count );
			let offset = 0;

			for (const node of nodes) {
				if (node.type === 'Mesh') {
					var buffer = node.geometry.attributes[ 'position' ];
					combined.set( buffer.array, offset );
					offset += buffer.array.length;
				}
				for (const child of node.children) {
					if (child.type === 'Mesh') {
						var buffer = child.geometry.attributes[ 'position' ];
						combined.set( buffer.array, offset );
						offset += buffer.array.length;
					}
				}
			}
			var attrPos = new THREE.BufferAttribute( combined, 3 );
			var geometry = new THREE.BufferGeometry();
			geometry.setAttribute( 'position', attrPos );
			// geometry.setAttribute( 'initialPos', attrPos.clone() );
			geometry.attributes.position.setUsage( THREE.DynamicDrawUsage );
			var mesh = new THREE.Points( geometry, new THREE.PointsMaterial( {
					size: paras.size, color: paras.color || xutils.randomColor() } ) );

			return mesh;
		}
	}

	static replaceMaterials(mesh, vparas, materl) {
		// Design Memo: TODO verify that sharing material is safe
		// FIXME is this the reason of the issue (bug)?
		if (!materl) {
			// TODO support ShaderFlag?
			materl = new THREE.PointsMaterial( {
							size: vparas.size,
							color: vparas.color || xutils.randomColor() } );
		}

		var grp = new THREE.Object3D();

		if (Array.isArray(mesh)) {
			for (var m of mesh)
				rebuildPoints(grp, m, materl, vparas);
		}
		else if (mesh instanceof THREE.Mesh) {
			// mesh.material = materl;
			rebuildPoints(grp, mesh, materl, vparas);
		}
		return grp;

		function rebuildPoints(root, oldMesh, materl, vparas) {
			var mesh = new THREE.Points( oldMesh.geometry, materl );
			mesh.quaternion.copy(oldMesh.quaternion);
			mesh.position.copy(oldMesh.position);
			mesh.scale.copy(oldMesh.scale);
			root.add(mesh);
			if (vparas.keepMaterial && oldMesh.material) {
				root.add(oldMesh);
				Object.assign(oldMesh.material, vparas.keepMaterial);
				oldMesh.material.transparent = true;
			}

			if (oldMesh.children && oldMesh.children.length > 0) {
				for (var child of oldMesh.children) {
					rebuildPoints(mesh, child, materl, vparas);
				}
			}
			return root;
		}
	}

	/**
	 * Create particles (THREE.Points) from vertices of mesh.
	 * @param {Obj3} cmpObj3 this enitiy's Obj3 component, uniforms merged to target mesh
	 * @param {THREE.Mesh} fromesh referencing mesh
	 * @param {Visual} cmpVisual this entity's Visual component.
	 * cmVisual.paras.u_tex: texture path
	 * cmVisual.shader: parameters for generate vertex and framgment shaders. see xglsl.xvShader().
	 * @return {THREE.Points} where geometry is generated by {@link xglsl.particlesGeom()},
	 * material is THREE.ShaderMaterial.
	 * @member Thrender.createObj3particles
	 * @function
	 */
	static createObj3particles(cmpObj3, fromesh, cmpVisual) {
		var m0 = fromesh.geometry.attributes[ 'position' ];
		// var material;
		var uniforms = new Object();
		if (cmpVisual.paras && cmpVisual.paras.u_tex) {
			uniforms = {
				// FIXME we should use AssetKeepr.loadTexure(), but it's supposed asynchronous
				// So we need notifying events deep into shaders?
				u_tex: { value: new THREE.TextureLoader().load(`assets/${cmpVisual.paras.u_tex}`) }
			};
		}
		else if ((cmpVisual.shader & ShaderFlag.defaultex) === ShaderFlag.defaultex) {
			uniforms = {
				u_tex: { value: AssetKeepr.defaultex() }
			};
		}

		Object.assign(uniforms, GlUniform.obj2uniforms(cmpObj3.uniforms));
		var material = Thrender.createXShaderMaterial(cmpVisual.shader, uniforms, cmpVisual.paras, cmpObj3);

		var destEnt;
		if (cmpVisual.vtype === AssetType.refPoint
			&& (!cmpVisual.paras || (!cmpVisual.paras.a_dest && !cmpVisual.paras.dest))) {
			console.error('Thrender.createObj3particles(): refPoints doesn\'t have a morphing a_dest.',
						  cmpObj3.entity);
		}
		else {
			destEnt = x.ecs.getEntity( cmpVisual.paras.a_dest || cmpVisual.paras.dest );
		}
		if ((cmpVisual.asset || cmpVisual.paras.dest || cmpVisual.paras.a_dest)
			&& !destEnt)
			console.error("The entity referenced by Visual.asset or Visual.paras.dest not found.");
		else {
			var geometry = xglsl.particlesGeom(cmpVisual.paras, m0,
					destEnt ? destEnt.Obj3.mesh.geometry.attributes['position'] : undefined);
			var mesh = new THREE.Points( geometry, material );
			return mesh;
		}
	}

	/**
	 * Create cube voxel. (points evenly dividing in cube).
	 *
	 * Return mesh object can be rendered by {@link Thrender}.<br>
	 * {@link AssetType}: AssetType.refPoint; AssetType.cubeVoxel
	 * @param {Obj3} cmpObj3
	 * @param {Visual} cmpVisual
	 * @return {THREE.Mesh}
	 * @member Thrender.createCubeVoxel
	 * @function
	 */
	static createCubeVoxel(cmpObj3, cmpVisual) {
		// FIXME can this uniforms been orthogonal handled?

		var uniforms = GlUniform.init(cmpVisual, cmpObj3);

		var {vertexShader, fragmentShader} = xglsl.xvShader(cmpVisual.shader, cmpVisual.paras);

		for (var bx = 0; bx < cmpVisual.paras.uniforms.u_cubes.length; bx++) {
			var destcube = cmpVisual.paras.uniforms.u_cubes[bx];
			if (!destcube) {
				console.error(`The visual paras.u_cubes[${destcube}] entity can't been found:\n`, cmpVisual);
				continue;
			}
			var entCube = x.ecs.getEntity(destcube);
			if (destcube && (!entCube || !entCube.Obj3.mesh || !entCube.Obj3.mesh.geometry.attributes['position'])) {
				console.error(`The visual paras.u_cubes element is ${destcube}, but no target mesh was found.`);
				continue;
			}
			else if ( !(entCube.Obj3.mesh.geometry instanceof THREE.BoxBufferGeometry)
				&& !(entCube.Obj3.mesh.geometry instanceof THREE.BoxGeometry) ) {
				if (x.log >= 3)
					console.warn(`[3] Can't create cube voxel from geometry other than box. Visual.paras.a_cube = ${destcube}`);
			}

			var u_boxi = `u_box${bx}`;
			var u_transi = `u_trans${bx}`;
			uniforms[u_boxi] = {value: new vec3(entCube.Obj3.box)};
			uniforms[u_transi] = {value: mat4.js(entCube.Obj3.mesh.matrix)};
		}

		var material = new THREE.ShaderMaterial( {
			uniforms,
			vertexShader,
			fragmentShader,
			blending: THREE.AdditiveBlending,
			depthTest: true,
			transparent: true,
		} );

		var geometry = xglsl.cubeVoxelGeom(cmpVisual.paras.uniforms);
		var mesh = new THREE.Points( geometry, material );
		return mesh;
	}

	/**Setup light of <a href='https://threejs.org/docs/index.html#api/en/lights/DirectionalLight'>
	 * THREE.DirectionalLight</a> &amp; <a href='https://threejs.org/docs/index.html#api/en/lights/shadows/DirectionalLightShadow'>
	 * THREE.DirectionalLightShadow</a>.<br>
	 * For parameters' example, see test/html/ecs-basics/light.html
	 * @param {object} options
	 * @param {object} [options.light]
	 * @param {number} [otpions.light.skyColor=0xffffbb]
	 * @param {number} [options.light.groundColor=0x080820]
	 * @param {array} [options.light.pos=[0, 1, 0.56]]
	 * @param {number} [options.light.intensity=1.1]
	 * @param {number} [otpions.light.castShadow=true]
	 * @param {object} [options.shadow]
	 * @param {object} [options.shadow.mapSize={width: 1024, height: 1024}]
	 * @param {object} [options.shadow.camera={near: 50, far: 2000}]
	 * @param {number} [options.shadow.size=20] The shadow cull space size,
	 * scaled to default THREE.shadowCamera.width, height, like:<pre>
	light.shadow.camera.left *= size;
	light.shadow.camera.right *= size;
	light.shadow.camera.bottom *= size;
	light.shadow.camera.top *= size;</pre>

	 * @param {object} [cam] {near, far}, camera's near, far
	 * @return {THREE.HemisphereLight} with options = arg-options
	 * @member Thrender.createCubeVoxel
	 * @function
	 */
	static createLight (options = {}, cam = {}) {
		var s = options.shadow || {}; // for short
		var l = options.light || {};

		var sky = l.skyColor || 0xffffbb;
		var ground = l.groundColor || 0xffffbb;
		l.diffuse = xutils.cssColor(sky);
		l.ambient = xutils.cssColor(ground);

		// var light = new THREE.HemisphereLight( sky, ground, options.u_lightIntensity );
		var light = new THREE.DirectionalLight ( sky, l.u_lightIntensity );
		light.groundColor = ground;

		light.castShadow = true;
		/*
		light.shadow.mapSize.width = 1024;  // default
		light.shadow.mapSize.height = 1024; // default
		light.shadow.bias = 0.00;

		light.shadow.camera.near = s.near !== undefined ? s.near : 50;
		light.shadow.camera.far  = s.far  !== undefined ? s.far : 2000;

		let ssz = s.size !== undefined ? s.size : 20;
		light.shadow.camera.left *= ssz;
		light.shadow.camera.right *= ssz;
		light.shadow.camera.bottom *= ssz;
		light.shadow.camera.top *= ssz;

		light.shadow.camera.zoom = s.zoom !== undefined ? s.zoom : 0.5;
		*/
		light.shadow.mapSize = Object.assign( {
			x: 1024, y: 1024,
			width: 1024,
			height: 1024
		}, s.mapSize);

		light.shadow.bias = s.bias === undefined ? 0.001 : s.bias;

		light.shadow.camera = Object.assign(light.shadow.camera,
			Object.assign(
				{ near: cam.near == undefined ? 1 : cam.near,
				  far: cam.far == undefined ? 2000 : cam.far },
				{ near: s.near, far: s.far} )
		);

		if (s.size === undefined || typeof s.size === 'number') {
			let ssz = s.size !== undefined ? s.size : 20;
			light.shadow.camera.left *= ssz;
			light.shadow.camera.right *= ssz;
			light.shadow.camera.top *= ssz;
			light.shadow.camera.bottom *= ssz;
		}
		else if (Array.isArray(s.size)) {
			light.shadow.camera.left *= s.size[0] || 20;
			light.shadow.camera.right *= s.size[1] || 20;
			light.shadow.camera.top *= s.size[2] || 20;
			light.shadow.camera.bottom *= s.size[3] || 20;
		}
		else if (typeof s.size === 'object') {
			light.shadow.camera.left *= s.size.left || 20;
			light.shadow.camera.right *= s.size.right || 20;
			light.shadow.camera.top *= s.size.top || 20;
			light.shadow.camera.bottom *= s.size.bottom || 20;
		}

		light.shadow.camera.zoom = s.zoom !== undefined ? s.zoom : 0.5;

		light.shadow.radius = s.radius === undefined ? 8 : s.radius;

		if (l && l.position) {
			light.position.x = l.position[0];
			light.position.y = l.position[1];
			light.position.z = l.position[2];
		}
		else {
			light.position.x = 100;
			light.position.y = 100;
			light.position.z = 56;
		}
		l.pos = l.position;

		light.layers.enableAll();

		light.options = l;
		light.options.hemisphere = light;
		return light;
	}

	/**
	 * @param {Canvas} canvas
	 * @param {x} x {options, ...}
	 * @member Thrender#init
	 * @function
	 */
	init(canvas, x) {
		this.camera = x.xcam.XCamera.cam;
		// renderer
		let opt = Object.assign(
				  { canvas: canvas, alpha: true, antialias: true },
					x.options.renderer )
		var renderer = new THREE.WebGLRenderer( opt );

		renderer.shadowMap.enabled = true;
		// renderer.shadowMap.type = opt.shadowMap.type;
		// renderer.shadowMapCullFace = THREE.CullFaceBack;

		if (! x.options.shadow)
			x.options.shadow = {};

		if (x.options.shadow) {
			renderer.shadowMap.enabled = true;
			renderer.shadowMap.type = x.options.shadow.type === undefined ?
						THREE.PCFSoftShadowMap : x.options.shadow.type;
			// renderer.shadowMapCullFace = opt.shadow.CullFace || THREE.CullFaceBack;
		}

		if (x.log >= 3)
			logGlInfo(renderer);

		var wh;
		if (x.options.canvasize) {
			wh = x.options.canvasize;
		}
		else {
			wh = [800, 400]
			x.options.canvasize = wh;
		}
		renderer.setSize( wh[0],  wh[1] );

		// document.body.appendChild( renderer.domElement );
		// scene
		var scene = new THREE.Scene();
		scene.layers.enableAll();
		scene.background = x.options.background || new THREE.Color('black');

		// light
		var light = Thrender.createLight(x.options);
		x.light = light;
		scene.add(light);

		if (x.options.shadow && x.options.shadow.helper) {
			var helper = new THREE.CameraHelper( light.shadow.camera );
			scene.add( helper );
		}

		this.scene = scene;
		this.renderer = renderer;
		x.scene = scene;
		x.renderer = renderer;

		// RenderPass for final composer
		var composer
			= new EffectComposer( this.renderer );
			var renderPass = new RenderPass( scene, this.camera );
			composer.addPass( renderPass );

		if (x.options.outlinePass) {
			composer = composer ? composer : new EffectComposer( this.renderer );
			this.outlinePass = new OutlinePass( new THREE.Vector2( wh[0], wh[1] ), scene, this.camera );
			this.outlinePass.selectedObjects = [];
			composer.addPass( this.outlinePass );
			this.composer = composer;
		}

		x.thrender = composer;	// why not this?
		return x;
	}

	/**1. Show picked object outline.<br>
	 * 2. Render with composer or scene render.
	 * @param {Number} tick
	 * @param {Array.<Entity>} entities
	 * @member Thrender#update
	 * @function
	 */
	update(tick, entities) {
		if (this.outlinePass) {
			this.outlinePass.selectedObjects.splice(0);
			for (var e of entities) { // high light picking, any better way?
				var pk = e.GpuPickable;
				if (pk && pk.picktick > 0 && pk.picked) {
					this.outlinePass.selectedObjects.push(e.Obj3.mesh);
					break;
				}
			}
		}

		if (this.camera) {
			this.camera.updateProjectionMatrix();
			if (!this.composer)
				this.renderer.render(this.scene, this.camera);
			else // with outlinePass
				this.composer.render();
		}
		else if (x.log >= 5)
			console.warn('[5] Thrender.update(): No camera, testing?');
	}
}

Thrender.query = {any};

function logGlInfo (renderer) {
	var gl = renderer.getContext();
	console.log('[3]', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
	console.log('[3] THREE.Revision', THREE.REVISION);
}