Source: render/WebGL/WebGLCore.js

import { CULL_FACE_TYPE, BLEND_TYPE, DRAW_SIDE, OBJECT_TYPE, WEBGL_PIXEL_TYPE, WEBGL_PIXEL_FORMAT, WEBGL_TEXTURE_FILTER } from '../../const.js';
import { nextPowerOfTwo } from '../../base.js';
import { Vector3 } from '../../math/Vector3.js';
import { Vector4 } from '../../math/Vector4.js';
import { Plane } from '../../math/Plane.js';
import { Texture2D } from '../../texture/Texture2D.js';
import { WebGLPrograms } from './WebGLPrograms.js';
import { WebGLProperties } from './WebGLProperties.js';
import { WebGLCapabilities } from './WebGLCapabilities.js';
import { WebGLState } from './WebGLState.js';
import { WebGLTexture } from './WebGLTexture.js';
import { WebGLRenderBuffer } from './WebGLRenderBuffer.js';
import { WebGLRenderTarget } from './WebGLRenderTarget.js';
import { WebGLGeometry } from './WebGLGeometry.js';
import { WebGLVertexArrayBindings } from './WebGLVertexArrayBindings.js';

var helpVector3 = new Vector3();
var helpVector4 = new Vector4();

var influencesList = new WeakMap();
var morphInfluences = new Float32Array(8);

function defaultGetMaterial(renderable) {
	return renderable.material;
}

function defaultIfRender(renderable) {
	return true;
}

function noop() {}

var planesData;
var helpPlane = new Plane();

function getClippingPlanesData(planes, camera) {
	if (!planesData || planesData.length < planes.length * 4) {
		planesData = new Float32Array(planes.length * 4);
	}

	for (var i = 0; i < planes.length; i++) {
		helpPlane.copy(planes[i]);// .applyMatrix4(camera.viewMatrix);
		planesData[i * 4 + 0] = helpPlane.normal.x;
		planesData[i * 4 + 1] = helpPlane.normal.y;
		planesData[i * 4 + 2] = helpPlane.normal.z;
		planesData[i * 4 + 3] = helpPlane.constant;
	}
	return planesData;
}

/**
 * Core render methods by WebGL.
 * @constructor
 * @memberof zen3d
 * @param {WebGLRenderingContext} gl
 */
function WebGLCore(gl) {
	this.gl = gl;

	var properties = new WebGLProperties();
	this.properties = properties;

	var capabilities = new WebGLCapabilities(gl);

	/**
     * An object containing details about the capabilities of the current RenderingContext.
     * @type {zen3d.WebGLCapabilities}
     */
	this.capabilities = capabilities;

	var state = new WebGLState(gl, capabilities);
	this.state = state;

	var vertexArrayBindings = new WebGLVertexArrayBindings(gl, properties, capabilities);
	this.vertexArrayBindings = vertexArrayBindings;

	var texture = new WebGLTexture(gl, state, properties, capabilities);
	this.texture = texture;

	var renderBuffer = new WebGLRenderBuffer(gl, properties, capabilities);

	this.renderTarget = new WebGLRenderTarget(gl, state, texture, renderBuffer, properties, capabilities);

	this.geometry = new WebGLGeometry(gl, state, vertexArrayBindings, properties, capabilities);

	this.programs = new WebGLPrograms(gl, state, capabilities);

	this._usedTextureUnits = 0;
}

Object.assign(WebGLCore.prototype, /** @lends zen3d.WebGLCore.prototype */{

	/**
     * Clear buffers.
     * @param {boolean} [color=false]
     * @param {boolean} [depth=false]
     * @param {boolean} [stencil=false]
     */
	clear: function(color, depth, stencil) {
		var gl = this.gl;

		var bits = 0;

		if (color === undefined || color) bits |= gl.COLOR_BUFFER_BIT;
		if (depth === undefined || depth) bits |= gl.DEPTH_BUFFER_BIT;
		if (stencil === undefined || stencil) bits |= gl.STENCIL_BUFFER_BIT;

		gl.clear(bits);
	},

	/**
     * Render opaque and transparent objects.
     * @param {zen3d.Scene} scene
     * @param {zen3d.Camera} camera
     * @param {boolean} [updateRenderList=true]
     */
	render: function(scene, camera, updateRenderList) {
		updateRenderList = (updateRenderList !== undefined ? updateRenderList : true);
		var renderList;
		if (updateRenderList) {
			renderList = scene.updateRenderList(camera);
		} else {
			renderList = scene.getRenderList(camera);
		}

		this.renderPass(renderList.opaque, camera, {
			scene: scene,
			getMaterial: function(renderable) {
				return scene.overrideMaterial || renderable.material;
			}
		});

		this.renderPass(renderList.transparent, camera, {
			scene: scene,
			getMaterial: function(renderable) {
				return scene.overrideMaterial || renderable.material;
			}
		});
	},

	/**
     * Render a single renderable list in camera in sequence.
     * @param {Array} list - List of all renderables.
     * @param {zen3d.Camera} camera - Camera provide view matrix and porjection matrix.
     * @param {Object} [config=] - The config for this render.
     * @param {Function} [config.getMaterial=] - Get renderable material.
     * @param {Function} [config.beforeRender=] - Before render each renderable.
     * @param {Function} [config.afterRender=] - After render each renderable
     * @param {Function} [config.ifRender=] - If render the renderable.
     * @param {zen3d.Scene} [config.scene=] - Rendering scene, have some rendering context.
     */
	renderPass: function(renderList, camera, config) {
		config = config || {};

		var state = this.state;
		var capabilities = this.capabilities;

		var getMaterial = config.getMaterial || defaultGetMaterial;
		var beforeRender = config.beforeRender || noop;
		var afterRender = config.afterRender || noop;
		var ifRender = config.ifRender || defaultIfRender;
		var scene = config.scene || {};

		var currentRenderTarget = state.currentRenderTarget;

		for (var i = 0, l = renderList.length; i < l; i++) {
			var renderItem = renderList[i];

			if (!ifRender(renderItem)) {
				continue;
			}

			var object = renderItem.object;
			var material = getMaterial.call(this, renderItem);
			var geometry = renderItem.geometry;
			var group = renderItem.group;

			object.onBeforeRender(renderItem, material);
			beforeRender.call(this, renderItem, material);

			var materialProperties = this.properties.get(material);
			if (material.needsUpdate === false) {
				if (materialProperties.program === undefined) {
					material.needsUpdate = true;
				} else if (materialProperties.fog !== scene.fog) {
					material.needsUpdate = true;
				} else if (scene.clippingPlanes && scene.clippingPlanes.length !==  materialProperties.numClippingPlanes) {
					material.needsUpdate = true;
				} else if (camera.outputEncoding !==  materialProperties.outputEncoding ||
                    camera.gammaFactor !==  materialProperties.gammaFactor) {
					material.needsUpdate = true;
				} else if (capabilities.version > 1 && scene.disableShadowSampler !== materialProperties.disableShadowSampler) {
					material.needsUpdate = true;
				} else {
					var acceptLight = material.acceptLight && !!scene.lights && scene.lights.totalNum > 0;
					if (acceptLight !== materialProperties.acceptLight) {
						material.needsUpdate = true;
					} else if (acceptLight) {
						if (!scene.lights.hash.compare(materialProperties.lightsHash) ||
							object.receiveShadow !== materialProperties.receiveShadow ||
							object.shadowType !== materialProperties.shadowType) {
							material.needsUpdate = true;
						}
					}
				}
			}
			if (material.needsUpdate) {
				if (materialProperties.program === undefined) {
					material.addEventListener('dispose', this.onMaterialDispose, this);
				}
				var oldProgram = materialProperties.program;
				materialProperties.program = this.programs.getProgram(camera, material, object, scene);
				if (oldProgram) {
					this.programs.releaseProgram(oldProgram);
				}
				materialProperties.fog = scene.fog;

				if (scene.lights) {
					materialProperties.acceptLight = material.acceptLight;
					materialProperties.lightsHash = scene.lights.hash.copyTo(materialProperties.lightsHash);
					materialProperties.receiveShadow = object.receiveShadow;
					materialProperties.shadowType = object.shadowType;
				} else {
					materialProperties.acceptLight = false;
				}

				materialProperties.numClippingPlanes = scene.clippingPlanes ? scene.clippingPlanes.length : 0;
				materialProperties.outputEncoding = camera.outputEncoding;
				materialProperties.gammaFactor = camera.gammaFactor;
				materialProperties.disableShadowSampler = scene.disableShadowSampler;

				material.needsUpdate = false;
			}
			var program = materialProperties.program;
			state.setProgram(program);

			this.geometry.setGeometry(geometry);

			// update morph targets
			if (object.morphTargetInfluences) {
				this.updateMorphtargets(object, geometry, program);
			}

			this.vertexArrayBindings.setup(object, geometry, program);

			// update uniforms
			var uniforms = program.uniforms;

			// upload light uniforms
			// shadow map need upload first ?
			// or it will cause bug
			if (material.acceptLight && scene.lights) {
				this.uploadLights(uniforms, scene.lights, scene.disableShadowSampler);
			}

			// upload bone matrices
			if (object.type === OBJECT_TYPE.SKINNED_MESH) {
				this.uploadSkeleton(uniforms, object, program.program);
			}

			// upload other uniforms
			for (var n = 0, ll = uniforms.seq.length; n < ll; n++) {
				var uniform = uniforms.seq[n];
				var key = uniform.id;

				// upload custom uniforms
				if (material.uniforms && material.uniforms[key] !== undefined) {
					uniform.set(material.uniforms[key], this);
					continue;
				}

				switch (key) {
				// pvm matrix
				case "u_Projection":
					uniform.set(camera.projectionMatrix.elements);
					break;
				case "u_View":
					uniform.set(camera.viewMatrix.elements);
					break;
				case "u_Model":
					var modelMatrix = object.worldMatrix.elements;
					uniform.set(modelMatrix);
					break;

				case "u_Color":
					var color = material.diffuse;
					uniform.setValue(color.r, color.g, color.b);
					break;
				case "u_Opacity":
					uniform.set(material.opacity);
					break;

				case "diffuseMap":
					uniform.set(material.diffuseMap, this);
					break;
				case "alphaMap":
					uniform.set(material.alphaMap, this);
					break;
				case "normalMap":
					uniform.set(material.normalMap, this);
					break;
				case "bumpMap":
					uniform.set(material.bumpMap, this);
					break;
				case "bumpScale":
					uniform.set(material.bumpScale);
					break;
				case "envMap":
					uniform.set(material.envMap, this);
					break;
				case "cubeMap":
					uniform.set(material.cubeMap, this);
					break;

				case "u_EnvMap_Intensity":
					uniform.set(material.envMapIntensity);
					break;
				case "maxMipLevel":
					uniform.set(this.properties.get(material.envMap).__maxMipLevel || 0);
					break;
				case "u_Specular":
					uniform.set(material.shininess);
					break;
				case "u_SpecularColor":
					var color = material.specular;
					uniform.setValue(color.r, color.g, color.b);
					break;
				case "specularMap":
					uniform.set(material.specularMap, this);
					break;
				case "aoMap":
					uniform.set(material.aoMap, this);
					break;
				case "aoMapIntensity":
					uniform.set(material.aoMapIntensity);
					break;
				case "u_Roughness":
					uniform.set(material.roughness);
					break;
				case "roughnessMap":
					uniform.set(material.roughnessMap, this);
					break;
				case "u_Metalness":
					uniform.set(material.metalness);
					break;
				case "metalnessMap":
					uniform.set(material.metalnessMap, this);
					break;
				case "glossiness":
					uniform.set(material.glossiness);
					break;
				case "glossinessMap":
					uniform.set(material.glossinessMap, this);
					break;
				case "emissive":
					var color = material.emissive;
					var intensity = material.emissiveIntensity;
					uniform.setValue(color.r * intensity, color.g * intensity, color.b * intensity);
					break;
				case "emissiveMap":
					uniform.set(material.emissiveMap, this);
					break;
				case "u_CameraPosition":
					helpVector3.setFromMatrixPosition(camera.worldMatrix);
					uniform.setValue(helpVector3.x, helpVector3.y, helpVector3.z);
					break;
				case "u_FogColor":
					var color = scene.fog.color;
					uniform.setValue(color.r, color.g, color.b);
					break;
				case "u_FogDensity":
					uniform.set(scene.fog.density);
					break;
				case "u_FogNear":
					uniform.set(scene.fog.near);
					break;
				case "u_FogFar":
					uniform.set(scene.fog.far);
					break;
				case "u_PointSize":
					uniform.set(material.size);
					break;
				case "u_PointScale":
					var scale = currentRenderTarget.height * 0.5; // three.js do this
					uniform.set(scale);
					break;
				case "dashSize":
					uniform.set(material.dashSize);
					break;
				case "totalSize":
					uniform.set(material.dashSize + material.gapSize);
					break;
				case "scale":
					uniform.set(material.scale);
					break;
				case "matcap":
					uniform.set(material.matcap, this);
					break;
				case "clippingPlanes":
					var planesData = getClippingPlanesData(scene.clippingPlanes || [], camera);
					uniform.set(planesData);
					break;
				case "uvTransform":
					var uvScaleMap;
					uvScaleMap = material.diffuseMap ||
                            material.specularMap || material.normalMap || material.bumpMap ||
                            material.roughnessMap || material.metalnessMap || material.emissiveMap;
					if (uvScaleMap) {
						if (uvScaleMap.matrixAutoUpdate) {
							uvScaleMap.updateMatrix();
						}
						uniform.set(uvScaleMap.matrix.elements);
					}
					break;
				case "alphaMapUVTransform":
					material.alphaMap.updateMatrix();
					uniform.set(material.alphaMap.matrix.elements);
				default:
					break;
				}
			}

			var frontFaceCW = object.worldMatrix.determinant() < 0;
			this.setStates(material, frontFaceCW);

			var viewport = helpVector4.set(
				currentRenderTarget.width,
				currentRenderTarget.height,
				currentRenderTarget.width,
				currentRenderTarget.height
			).multiply(camera.rect);

			viewport.z -= viewport.x;
			viewport.w -= viewport.y;

			viewport.x = Math.floor(viewport.x);
			viewport.y = Math.floor(viewport.y);
			viewport.z = Math.floor(viewport.z);
			viewport.w = Math.floor(viewport.w);

			state.viewport(viewport.x, viewport.y, viewport.z, viewport.w);

			this.draw(geometry, material, group);

			this.vertexArrayBindings.resetBinding();

			// reset used tex Unit
			this._usedTextureUnits = 0;

			// Ensure depth buffer writing is enabled so it can be cleared on next render

			state.depthBuffer.setTest(true);
			state.depthBuffer.setMask(true);
			state.colorBuffer.setMask(true);

			afterRender(this, renderItem);
			object.onAfterRender(renderItem);
		}
	},

	// Set states.
	setStates: function(material, frontFaceCW) {
		var state = this.state;

		// set draw side
		state.setCullFace(
			(material.side === DRAW_SIDE.DOUBLE) ? CULL_FACE_TYPE.NONE : CULL_FACE_TYPE.BACK
		);

		var flipSided = (material.side === DRAW_SIDE.BACK);
		if (frontFaceCW) flipSided = !flipSided;

		state.setFlipSided(flipSided);

		// set blend state
		if (material.transparent) {
			state.setBlend(material.blending, material.blendEquation, material.blendSrc, material.blendDst, material.blendEquationAlpha, material.blendSrcAlpha, material.blendDstAlpha, material.premultipliedAlpha);
		} else {
			state.setBlend(BLEND_TYPE.NONE);
		}

		// set buffers state
		state.depthBuffer.setFunc(material.depthFunc);
		state.depthBuffer.setTest(material.depthTest);
		state.depthBuffer.setMask(material.depthWrite);
		state.colorBuffer.setMask(material.colorWrite);

		// set stencil buffers
		var stencilTest = material.stencilTest;
		state.stencilBuffer.setTest(stencilTest);
		if (stencilTest) {
			state.stencilBuffer.setMask(material.stencilWriteMask);
			state.stencilBuffer.setFunc(material.stencilFunc, material.stencilRef, material.stencilFuncMask, material.stencilFuncBack, material.stencilRefBack, material.stencilFuncMaskBack);
			state.stencilBuffer.setOp(material.stencilFail, material.stencilZFail, material.stencilZPass, material.stencilFailBack, material.stencilZFailBack, material.stencilZPassBack);
		}

		state.setPolygonOffset(material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits);

		// set line width
		if (material.lineWidth !== undefined) {
			state.setLineWidth(material.lineWidth);
		}
	},

	// GL draw.
	draw: function(geometry, material, group) {
		var gl = this.gl;
		var properties = this.properties;
		var capabilities = this.capabilities;

		var useIndexBuffer = geometry.index !== null;

		var drawStart = 0;
		var drawCount = useIndexBuffer ? geometry.index.count : geometry.getAttribute("a_Position").count;
		var groupStart = group ? group.start : 0;
		var groupCount = group ? group.count : Infinity;
		drawStart = Math.max(drawStart, groupStart);
		drawCount = Math.min(drawCount, groupCount);

		if (useIndexBuffer) {
			var indexProperty = properties.get(geometry.index);
			var bytesPerElement = indexProperty.bytesPerElement;
			var type = indexProperty.type;

			if (type === gl.UNSIGNED_INT) {
				if (capabilities.version < 2 && !capabilities.getExtension('OES_element_index_uint')) {
					console.warn("draw elements type not support UNSIGNED_INT!");
				}
			}

			if (geometry.isInstancedGeometry) {
				if (geometry.maxInstancedCount > 0) {
					if (capabilities.version >= 2) {
						gl.drawElementsInstanced(material.drawMode, drawCount, type, drawStart * bytesPerElement, geometry.maxInstancedCount);
					} else if (capabilities.getExtension('ANGLE_instanced_arrays')) {
						capabilities.getExtension('ANGLE_instanced_arrays').drawElementsInstancedANGLE(material.drawMode, drawCount, type, drawStart * bytesPerElement, geometry.maxInstancedCount);
					} else {
						console.warn("no support instanced draw.");
					}
				}
			} else {
				gl.drawElements(material.drawMode, drawCount, type, drawStart * bytesPerElement);
			}
		} else {
			if (geometry.isInstancedGeometry) {
				if (geometry.maxInstancedCount > 0) {
					if (capabilities.version >= 2) {
						gl.drawArraysInstanced(material.drawMode, drawStart, drawCount, geometry.maxInstancedCount);
					} else if (capabilities.getExtension('ANGLE_instanced_arrays')) {
						capabilities.getExtension('ANGLE_instanced_arrays').drawArraysInstancedANGLE(material.drawMode, drawStart, drawCount, geometry.maxInstancedCount);
					} else {
						console.warn("no support instanced draw.");
					}
				}
			} else {
				gl.drawArrays(material.drawMode, drawStart, drawCount);
			}
		}
	},

	// Upload skeleton uniforms.
	uploadSkeleton: function(uniforms, object, programId) {
		if (object.skeleton && object.skeleton.bones.length > 0) {
			var skeleton = object.skeleton;
			var capabilities = this.capabilities;

			skeleton.updateBones();

			if (capabilities.maxVertexTextures > 0 && (!!capabilities.getExtension('OES_texture_float') || capabilities.version >= 2)) {
				if (skeleton.boneTexture === undefined) {
					var size = Math.sqrt(skeleton.bones.length * 4);
					size = nextPowerOfTwo(Math.ceil(size));
					size = Math.max(4, size);

					var boneMatrices = new Float32Array(size * size * 4);
					boneMatrices.set(skeleton.boneMatrices);
					var boneTexture = new Texture2D();
					boneTexture.image = { data: boneMatrices, width: size, height: size };
					if (capabilities.version >= 2) {
						boneTexture.internalformat = WEBGL_PIXEL_FORMAT.RGBA32F;
						boneTexture.format = WEBGL_PIXEL_FORMAT.RGBA;
					}
					boneTexture.type = WEBGL_PIXEL_TYPE.FLOAT;
					boneTexture.magFilter = WEBGL_TEXTURE_FILTER.NEAREST;
					boneTexture.minFilter = WEBGL_TEXTURE_FILTER.NEAREST;
					boneTexture.generateMipmaps = false;
					boneTexture.flipY = false;

					skeleton.boneMatrices = boneMatrices;
					skeleton.boneTexture = boneTexture;
				}

				uniforms.set("boneTexture", skeleton.boneTexture, this);
				uniforms.set("boneTextureSize", skeleton.boneTexture.image.width);
			} else {
				uniforms.set("boneMatrices", skeleton.boneMatrices);
			}

			uniforms.set("bindMatrix", object.bindMatrix.elements);
			uniforms.set("bindMatrixInverse", object.bindMatrixInverse.elements);
		}
	},

	// Upload lights uniforms.
	uploadLights: function(uniforms, lights, disableShadowSampler) {
		if (lights.ambientsNum > 0) {
			uniforms.set("u_AmbientLightColor", lights.ambient);
		}

		if (lights.directsNum > 0) {
			uniforms.set("u_Directional", lights.directional);

			if (uniforms.has("directionalShadowMap")) {
				if (this.capabilities.version >= 2 && !disableShadowSampler) {
					uniforms.set("directionalShadowMap", lights.directionalShadowDepthMap, this);
				} else {
					uniforms.set("directionalShadowMap", lights.directionalShadowMap, this);
				}
				uniforms.set("directionalShadowMatrix", lights.directionalShadowMatrix);
			}

			if (uniforms.has("directionalDepthMap")) {
				uniforms.set("directionalDepthMap", lights.directionalShadowMap, this);
			}
		}

		if (lights.pointsNum > 0) {
			uniforms.set("u_Point", lights.point);

			if (uniforms.has("pointShadowMap")) {
				uniforms.set("pointShadowMap", lights.pointShadowMap, this);
			}
		}

		if (lights.spotsNum > 0) {
			uniforms.set("u_Spot", lights.spot);

			if (uniforms.has("spotShadowMap")) {
				if (this.capabilities.version >= 2 && !disableShadowSampler) {
					uniforms.set("spotShadowMap", lights.spotShadowDepthMap, this);
				} else {
					uniforms.set("spotShadowMap", lights.spotShadowMap, this);
				}
				uniforms.set("spotShadowMatrix", lights.spotShadowMatrix);
			}

			if (uniforms.has("spotDepthMap")) {
				uniforms.set("spotDepthMap", lights.spotShadowMap, this);
			}
		}
	},

	// Alloc texture unit.
	allocTexUnit: function() {
		var textureUnit = this._usedTextureUnits;

		if (textureUnit >= this.capabilities.maxTextures) {
			console.warn('trying to use ' + textureUnit + ' texture units while this GPU supports only ' + this.capabilities.maxTextures);
		}

		this._usedTextureUnits += 1;

		return textureUnit;
	},

	updateMorphtargets: function(object, geometry, program) {
		var objectInfluences = object.morphTargetInfluences;

		if (!influencesList.has(geometry)) {
			influencesList.set(geometry, objectInfluences.slice(0));
		}

		var morphTargets = geometry.morphAttributes.position;
		var morphNormals = geometry.morphAttributes.normal;

		// Remove current morphAttributes

		var influences = influencesList.get(geometry);

		for (var i = 0; i < influences.length; i++) {
			var influence = influences[i];

			if (influence !== 0) {
				if (morphTargets) geometry.removeAttribute('morphTarget' + i);
				if (morphNormals) geometry.removeAttribute('morphNormal' + i);
			}
		}

		// Collect influences

		for (var i = 0; i < objectInfluences.length; i++) {
			influences[i] = objectInfluences[i];
		}

		influences.length = objectInfluences.length;

		// Add morphAttributes

		var count = 0;

		for (var i = 0; i < influences.length; i++) {
			var influence = influences[i];

			if (influence > 0) {
				if (morphTargets) geometry.addAttribute('morphTarget' + count, morphTargets[i]);
				if (morphNormals) geometry.addAttribute('morphNormal' + count, morphNormals[i]);

				morphInfluences[count] = influence;

				count++;
			}
		}

		for (;count < 8; count++) {
			morphInfluences[count] = 0;
		}

		program.uniforms.set('morphTargetInfluences', morphInfluences);
	},

	onMaterialDispose: function(event) {
		var material = event.target;
		var materialProperties = this.properties.get(material);

		material.removeEventListener('dispose', this.onMaterialDispose, this);

		var program = materialProperties.program;

		if (program !== undefined) {
			// release program reference
			this.programs.releaseProgram(program);
		}

		this.properties.delete(material);
	}

});

export { WebGLCore };