From dda4431366902ba9e490c4b3f372ef265ba21025 Mon Sep 17 00:00:00 2001 From: Richard Davey Date: Wed, 7 Oct 2020 17:44:36 +0100 Subject: [PATCH] Merged the Layer3D Game Object and pipeline back in for now --- src/display/RGB.js | 214 ++++ src/gameobjects/index.js | 3 + src/gameobjects/layer3d/Layer3D.js | 575 +++++++++++ src/gameobjects/layer3d/Layer3DCamera.js | 716 ++++++++++++++ .../layer3d/Layer3DCanvasRenderer.js | 22 + src/gameobjects/layer3d/Layer3DCreator.js | 40 + src/gameobjects/layer3d/Layer3DFactory.js | 38 + src/gameobjects/layer3d/Layer3DLight.js | 295 ++++++ src/gameobjects/layer3d/Layer3DRender.js | 25 + .../layer3d/Layer3DWebGLRenderer.js | 52 + src/geom/mesh/Model.js | 918 ++++++++++++++++++ src/geom/mesh/Vertex.js | 36 + src/renderer/webgl/PipelineManager.js | 2 + src/renderer/webgl/pipelines/MeshPipeline.js | 288 ++++++ src/renderer/webgl/pipelines/const.js | 12 +- src/renderer/webgl/shaders/Mesh-frag.js | 54 ++ src/renderer/webgl/shaders/Mesh-vert.js | 29 + src/renderer/webgl/shaders/src/Mesh.frag | 51 + src/renderer/webgl/shaders/src/Mesh.vert | 26 + 19 files changed, 3395 insertions(+), 1 deletion(-) create mode 100644 src/display/RGB.js create mode 100644 src/gameobjects/layer3d/Layer3D.js create mode 100644 src/gameobjects/layer3d/Layer3DCamera.js create mode 100644 src/gameobjects/layer3d/Layer3DCanvasRenderer.js create mode 100644 src/gameobjects/layer3d/Layer3DCreator.js create mode 100644 src/gameobjects/layer3d/Layer3DFactory.js create mode 100644 src/gameobjects/layer3d/Layer3DLight.js create mode 100644 src/gameobjects/layer3d/Layer3DRender.js create mode 100644 src/gameobjects/layer3d/Layer3DWebGLRenderer.js create mode 100644 src/geom/mesh/Model.js create mode 100644 src/renderer/webgl/pipelines/MeshPipeline.js create mode 100644 src/renderer/webgl/shaders/Mesh-frag.js create mode 100644 src/renderer/webgl/shaders/Mesh-vert.js create mode 100644 src/renderer/webgl/shaders/src/Mesh.frag create mode 100644 src/renderer/webgl/shaders/src/Mesh.vert diff --git a/src/display/RGB.js b/src/display/RGB.js new file mode 100644 index 000000000..ff9e1951f --- /dev/null +++ b/src/display/RGB.js @@ -0,0 +1,214 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var Class = require('../utils/Class'); +var NOOP = require('../utils/NOOP'); + +/** + * @classdesc + * The RGB class holds a single color value and allows for easy modification and reading of it, + * with optional on-change callback notification and a dirty flag. + * + * @class RGB + * @memberof Phaser.Display + * @constructor + * @since 3.50.0 + * + * @param {number} [red=0] - The red color value. A number between 0 and 1. + * @param {number} [green=0] - The green color value. A number between 0 and 1. + * @param {number} [blue=0] - The blue color value. A number between 0 and 1. + */ +var RGB = new Class({ + + initialize: + + function RGB (red, green, blue) + { + /** + * Cached RGB values. + * + * @name Phaser.Display.RGB#_rgb + * @type {number[]} + * @private + * @since 3.50.0 + */ + this._rgb = [ 0, 0, 0 ]; + + /** + * This callback will be invoked each time one of the RGB color values change. + * + * The callback is sent the new color values as the parameters. + * + * @name Phaser.Display.RGB#onChangeCallback + * @type {function} + * @since 3.50.0 + */ + this.onChangeCallback = NOOP; + + /** + * Is this color dirty? + * + * @name Phaser.Display.RGB#dirty + * @type {boolean} + * @since 3.50.0 + */ + this.dirty = false; + + this.set(red, green, blue); + }, + + /** + * Sets the red, green and blue values of this RGB object, flags it as being + * dirty and then invokes the `onChangeCallback`, if set. + * + * @method Phaser.Display.RGB#set + * @since 3.50.0 + * + * @param {number} [red=0] - The red color value. A number between 0 and 1. + * @param {number} [green=0] - The green color value. A number between 0 and 1. + * @param {number} [blue=0] - The blue color value. A number between 0 and 1. + * + * @return {this} This RGB instance. + */ + set: function (red, green, blue) + { + if (red === undefined) { red = 0; } + if (green === undefined) { green = 0; } + if (blue === undefined) { blue = 0; } + + this._rgb = [ red, green, blue ]; + + this.onChange(); + + return this; + }, + + /** + * Compares the given rgb parameters with those in this object and returns + * a boolean `true` value if they are equal, otherwise it returns `false`. + * + * @method Phaser.Display.RGB#equals + * @since 3.50.0 + * + * @param {number} red - The red value to compare with this object. + * @param {number} green - The green value to compare with this object. + * @param {number} blue - The blue value to compare with this object. + * + * @return {boolean} `true` if the given values match those in this object, otherwise `false`. + */ + equals: function (red, green, blue) + { + var rgb = this._rgb; + + return (rgb.r === red && rgb.g === green && rgb.b === blue); + }, + + /** + * Internal on change handler. Sets this object as being dirty and + * then invokes the `onChangeCallback`, if set, passing in the + * new RGB values. + * + * @method Phaser.Display.RGB#onChange + * @since 3.50.0 + */ + onChange: function () + { + this.dirty = true; + + var rgb = this._rgb; + + this.onChangeCallback.call(this, rgb[0], rgb[1], rgb[2]); + }, + + /** + * The red color value. Between 0 and 1. + * + * Changing this property will flag this RGB object as being dirty + * and invoke the `onChangeCallback` , if set. + * + * @name Phaser.Display.RGB#r + * @type {number} + * @since 3.50.0 + */ + r: { + + get: function () + { + return this._rgb[0]; + }, + + set: function (value) + { + this._rgb[0] = value; + this.onChange(); + } + + }, + + /** + * The green color value. Between 0 and 1. + * + * Changing this property will flag this RGB object as being dirty + * and invoke the `onChangeCallback` , if set. + * + * @name Phaser.Display.RGB#g + * @type {number} + * @since 3.50.0 + */ + g: { + + get: function () + { + return this._rgb[1]; + }, + + set: function (value) + { + this._rgb[1] = value; + this.onChange(); + } + + }, + + /** + * The blue color value. Between 0 and 1. + * + * Changing this property will flag this RGB object as being dirty + * and invoke the `onChangeCallback` , if set. + * + * @name Phaser.Display.RGB#b + * @type {number} + * @since 3.50.0 + */ + b: { + + get: function () + { + return this._rgb[2]; + }, + + set: function (value) + { + this._rgb[2] = value; + this.onChange(); + } + + }, + + /** + * Nulls any external references this object contains. + * + * @method Phaser.Display.RGB#destroy + * @since 3.50.0 + */ + destroy: function () + { + this.onChangeCallback = null; + } + +}); + +module.exports = RGB; diff --git a/src/gameobjects/index.js b/src/gameobjects/index.js index 98fa25320..1873724f1 100644 --- a/src/gameobjects/index.js +++ b/src/gameobjects/index.js @@ -124,12 +124,15 @@ if (typeof WEBGL_RENDERER) // WebGL only Game Objects GameObjects.Shader = require('./shader/Shader'); GameObjects.Mesh = require('./mesh/Mesh'); + GameObjects.Layer3D = require('./layer3d/Layer3D'); GameObjects.Factories.Shader = require('./shader/ShaderFactory'); GameObjects.Factories.Mesh = require('./mesh/MeshFactory'); + GameObjects.Factories.Layer3D = require('./layer3d/Layer3DFactory'); GameObjects.Creators.Shader = require('./shader/ShaderCreator'); GameObjects.Creators.Mesh = require('./mesh/MeshCreator'); + GameObjects.Creators.Layer3D = require('./layer3d/Layer3DCreator'); GameObjects.Light = require('./lights/Light'); GameObjects.LightsManager = require('./lights/LightsManager'); diff --git a/src/gameobjects/layer3d/Layer3D.js b/src/gameobjects/layer3d/Layer3D.js new file mode 100644 index 000000000..288cbacb7 --- /dev/null +++ b/src/gameobjects/layer3d/Layer3D.js @@ -0,0 +1,575 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var Class = require('../../utils/Class'); +var Components = require('../components'); +var CONST = require('../../renderer/webgl/pipelines/const'); +var GameObject = require('../GameObject'); +var GameObjectEvents = require('../events'); +var Layer3DCamera = require('./Layer3DCamera'); +var Layer3DLight = require('./Layer3DLight'); +var Layer3DRender = require('./Layer3DRender'); +var Model = require('../../geom/mesh/Model'); +var RGB = require('../../display/RGB'); + +/** + * @classdesc + * A Layer3D Game Object. + * + * The Mesh object is WebGL only and does not have a Canvas counterpart. + * + * TODO - Finish this. + * + * @class Layer3D + * @extends Phaser.GameObjects.GameObject + * @memberof Phaser.GameObjects + * @constructor + * @webglOnly + * @since 3.50.0 + * + * @extends Phaser.GameObjects.Components.AlphaSingle + * @extends Phaser.GameObjects.Components.BlendMode + * @extends Phaser.GameObjects.Components.Depth + * @extends Phaser.GameObjects.Components.Mask + * @extends Phaser.GameObjects.Components.Pipeline + * @extends Phaser.GameObjects.Components.Transform + * @extends Phaser.GameObjects.Components.Visible + * @extends Phaser.GameObjects.Components.ScrollFactor + * @extends Phaser.GameObjects.Components.Size + * + * @param {Phaser.Scene} scene - The Scene to which this Game Object belongs. A Game Object can only belong to one Scene at a time. + * @param {number} [x] - The horizontal position of this Game Object in the world. + * @param {number} [y] - The vertical position of this Game Object in the world. + */ +var Layer3D = new Class({ + + Extends: GameObject, + + Mixins: [ + Components.AlphaSingle, + Components.BlendMode, + Components.Depth, + Components.Mask, + Components.Pipeline, + Components.Transform, + Components.Visible, + Components.ScrollFactor, + Components.Size, + Layer3DRender + ], + + initialize: + + function Layer3D (scene, x, y) + { + GameObject.call(this, scene, 'Layer3D'); + + /** + * A Camera which can be used to control the view of the models being managed + * by this Layer3D. It will default to have an fov of 45 and be positioned at 0, 0, -10, + * with a near of 0.01 and far of 1000. You can change all of these by using the + * methods and properties available on the `Layer3DCamera` class. + * + * @name Phaser.GameObjects.Layer3D#camera + * @type {Phaser.GameObjects.Layer3DCamera} + * @since 3.50.0 + */ + this.camera = new Layer3DCamera(this, 45, 0, 0, -10, 0.01, 1000); + + /** + * An ambient light source for the entire Layer3D scene and all models it is rendering. + * + * It is created at a position of 0, -100, 0 with full ambient, diffuse and specular + * values. You can change all of these by using the methods and properties + * available on the `Layer3DLight` class. + * + * @name Phaser.GameObjects.Layer3D#light + * @type {Phaser.GameObjects.Layer3DLight} + * @since 3.50.0 + */ + this.light = new Layer3DLight(this, 0, 100, 0); + + /** + * The color of the fog. + * + * By default it is 0,0,0, which is black. + * + * @name Phaser.GameObjects.Layer3D#fogColor + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.fogColor = new RGB(); + + /** + * The minimum distance from which fog starts to affect objects closer than it. + * + * @name Phaser.GameObjects.Layer3D#fogNear + * @type {number} + * @since 3.50.0 + */ + this.fogNear = 0; + + /** + * The maximum distance from which fog starts to affect objects further than it. + * + * @name Phaser.GameObjects.Layer3D#fogFar + * @type {number} + * @since 3.50.0 + */ + this.fogFar = Infinity; + + /** + * An array of model instances that have been created in this Layer3D. + * + * This array can be sorted, by your own functions, to control model rendering order. + * + * @name Phaser.GameObjects.Layer3D#models + * @type {Phaser.Geom.Mesh.Model[]} + * @since 3.50.0 + */ + this.models = []; + + /** + * Internal cached value. + * + * @name Phaser.GameObjects.Layer3D#_prevWidth + * @type {number} + * @private + * @since 3.50.0 + */ + this._prevWidth = 0; + + /** + * Internal cached value. + * + * @name Phaser.GameObjects.Layer3D#_prevHeight + * @type {number} + * @private + * @since 3.50.0 + */ + this._prevHeight = 0; + + var renderer = scene.sys.renderer; + + this.setPosition(x, y); + this.setSize(renderer.width, renderer.height); + this.initPipeline(CONST.MESH_PIPELINE); + + this.on(GameObjectEvents.ADDED_TO_SCENE, this.addedToScene, this); + this.on(GameObjectEvents.REMOVED_FROM_SCENE, this.removedFromScene, this); + }, + + // Overrides Game Object method + addedToScene: function () + { + this.scene.sys.updateList.add(this); + }, + + // Overrides Game Object method + removedFromScene: function () + { + this.scene.sys.updateList.remove(this); + }, + + /** + * Removes all models from this Layer3D, calling `destroy` on each one of them. + * + * @method Phaser.GameObjects.Layer3D#clearModels + * @since 3.50.0 + */ + clearModels: function () + { + var models = this.models; + + for (var i = 0; i < models.length; i++) + { + models[i].destroy(); + } + + this.models = []; + }, + + /** + * This method creates a new blank Model instance and adds it to this Layer3D. + * + * You still need to tell it how many vertices it's going to contain in total, but you can + * populate the vertex data at a later stage after calling this. It won't try to render + * while it has no vertices. + * + * @method Phaser.GameObjects.Layer3D#addModel + * @since 3.50.0 + * + * @param {number} verticesCount - The total number of vertices this model can contain. + * @param {string|Phaser.Textures.Texture} [texture] - The key, or instance of the Texture this model will use to render with, as stored in the Texture Manager. + * @param {string|integer} [frame] - An optional frame from the Texture this model is rendering with. Ensure your UV data also matches this frame. + * @param {number} [x=0] - The x position of the Model. + * @param {number} [y=0] - The y position of the Model. + * @param {number} [z=0] - The z position of the Model. + * + * @return {Phaser.Geom.Mesh.Model} The model instance that was created. + */ + addModel: function (verticesCount, texture, frame, x, y, z) + { + var model = new Model(this, verticesCount, texture, frame, x, y, z); + + this.models.push(model); + + return model; + }, + + /** + * This method creates a new Model based on a loaded triangulated Wavefront OBJ. + * + * The obj file should have been loaded via OBJFile: + * + * ```javascript + * this.load.obj(key, url, [ flipUV ]); + * ``` + * + * Then use the key it was loaded under in this method. + * + * If the model has a texture, you must provide it as the second parameter. + * + * The model is then added to this Layer3D. A single Layer3D can contain multiple models + * without impacting each other. Equally, multiple models can all share the same base OBJ + * data. + * + * Make sure your 3D package has triangulated the model data prior to exporting it. + * + * You can scale the model data during import, which will set the new 'base' scale for the model. + * + * You can also offset the models generated vertex positions via the `originX`, `originY` and `originZ` + * parameters, which will change the rotation origin of the model. The model itself can be positioned, + * rotated and scaled independantly of these settings, so don't use them to position the model, just + * use them to offset the base values. + * + * @method Phaser.GameObjects.Layer3D#addModelFromOBJ + * @since 3.50.0 + * + * @param {string} key - The key of the data in the OBJ Cache to create the model from. + * @param {string|Phaser.Textures.Texture} [texture] - The key, or instance of the Texture this model will use to render with, as stored in the Texture Manager. + * @param {string|integer} [frame] - An optional frame from the Texture this model is rendering with. Ensure your UV data also matches this frame. + * @param {number} [scale=1] - An amount to scale the model data by during creation. + * @param {number} [originX=0] - The x origin of the model vertices during creation. + * @param {number} [originY=0] - The y origin of the model vertices during creation. + * @param {number} [originZ=0] - The z origin of the model vertices during creation. + * + * @return {Phaser.Geom.Mesh.Model|Phaser.Geom.Mesh.Model[]} The Model instance that was created. If the OBJ contained multiple models then an array of Model instances is returned. + */ + addModelFromOBJ: function (key, texture, frame, scale, originX, originY, originZ) + { + var model = []; + var data = this.scene.sys.cache.obj.get(key); + + if (data) + { + model = this.addModelFromData(data, texture, frame, scale, originX, originY, originZ); + } + + return (model.length === 1) ? model[0] : model; + }, + + /** + * This method creates a new Model based on the parsed triangulated model data. + * + * The data should have been parsed in advance via a function such as `ParseObj`: + * + * ```javascript + * const data = Phaser.Geom.Mesh.ParseObj(rawData, flipUV); + * + * Layer3D.addModelFromData(data, texture, frame); + * ``` + * + * If the model has a texture, you must provide it as the second parameter. + * + * The model is then added to this Layer3D. A single Layer3D can contain multiple models + * without impacting each other. Equally, multiple models can all share the same base OBJ + * data. + * + * Make sure your 3D package has triangulated the model data prior to exporting it. + * + * You can scale the model data during import, which will set the new 'base' scale for the model. + * + * You can also offset the models generated vertex positions via the `originX`, `originY` and `originZ` + * parameters, which will change the rotation origin of the model. The model itself can be positioned, + * rotated and scaled independantly of these settings, so don't use them to position the model, just + * use them to offset the base values. + * + * @method Phaser.GameObjects.Layer3D#addModelFromData + * @since 3.50.0 + * + * @param {array} data - The parsed model data. + * @param {string|Phaser.Textures.Texture} [texture] - The key, or instance of the Texture this model will use to render with, as stored in the Texture Manager. + * @param {string|integer} [frame] - An optional frame from the Texture this model is rendering with. Ensure your UV data also matches this frame. + * @param {number} [scale=1] - An amount to scale the model data by during creation. + * @param {number} [originX=0] - The x origin of the model vertices during creation. + * @param {number} [originY=0] - The y origin of the model vertices during creation. + * @param {number} [originZ=0] - The z origin of the model vertices during creation. + * + * @return {Phaser.Geom.Mesh.Model|Phaser.Geom.Mesh.Model[]} The Model instance that was created. If the data contained multiple models then an array of Model instances is returned. + */ + addModelFromData: function (data, texture, frame, scale, originX, originY, originZ) + { + if (scale === undefined) { scale = 1; } + if (originX === undefined) { originX = 0; } + if (originY === undefined) { originY = 0; } + if (originZ === undefined) { originZ = 0; } + + var results = []; + + for (var m = 0; m < data.models.length; m++) + { + var modelData = data.models[m]; + + var vertices = modelData.vertices; + var textureCoords = modelData.textureCoords; + var normals = modelData.vertexNormals; + var faces = modelData.faces; + + var model = this.addModel(faces.length * 3, texture, frame); + + var defaultUV1 = { u: 0, v: 1 }; + var defaultUV2 = { u: 0, v: 0 }; + var defaultUV3 = { u: 1, v: 1 }; + + for (var i = 0; i < faces.length; i++) + { + var face = faces[i]; + + // {textureCoordsIndex: 0, vertexIndex: 16, vertexNormalIndex: 16} + var v1 = face.vertices[0]; + var v2 = face.vertices[1]; + var v3 = face.vertices[2]; + + // {x: 0.19509, y: 0.980785, z: 0} + var m1 = vertices[v1.vertexIndex]; + var m2 = vertices[v2.vertexIndex]; + var m3 = vertices[v3.vertexIndex]; + + var n1 = normals[v1.vertexNormalIndex]; + var n2 = normals[v2.vertexNormalIndex]; + var n3 = normals[v3.vertexNormalIndex]; + + var t1 = v1.textureCoordsIndex; + var t2 = v2.textureCoordsIndex; + var t3 = v3.textureCoordsIndex; + + var uv1 = (t1 === -1) ? defaultUV1 : textureCoords[t1]; + var uv2 = (t2 === -1) ? defaultUV2 : textureCoords[t2]; + var uv3 = (t3 === -1) ? defaultUV3 : textureCoords[t3]; + + model.addVertex(originX + m1.x * scale, originY + m1.y * scale, originZ + m1.z * scale, uv1.u, uv1.v, n1.x, n1.y, n1.z); + model.addVertex(originX + m2.x * scale, originY + m2.y * scale, originZ + m2.z * scale, uv2.u, uv2.v, n2.x, n2.y, n2.z); + model.addVertex(originX + m3.x * scale, originY + m3.y * scale, originZ + m3.z * scale, uv3.u, uv3.v, n3.x, n3.y, n3.z); + } + + results.push(model); + } + + return results; + }, + + /** + * This method creates a new Model based on the given triangulated vertices arrays. + * + * Adds vertices to this model by parsing the given arrays. + * + * This method will take vertex data in one of two formats, based on the `containsZ` parameter. + * + * If your vertex data are `x`, `y` pairs, then `containsZ` should be `false` (this is the default) + * + * If your vertex data is groups of `x`, `y` and `z` values, then the `containsZ` parameter must be true. + * + * The `uvs` parameter is a numeric array consisting of `u` and `v` pairs. + * The `normals` parameter is a numeric array consisting of `x`, `y` vertex normal values and, if `containsZ` is true, `z` values as well. + * The `indicies` parameter is an optional array that, if given, is an indexed list of vertices to be added. + * + * The following example will create a 256 x 256 sized quad using an index array: + * + * ```javascript + * const vertices = [ + * -128, 128, + * 128, 128, + * -128, -128, + * 128, -128 + * ]; + * + * const uvs = [ + * 0, 1, + * 1, 1, + * 0, 0, + * 1, 0 + * ]; + * + * const indices = [ 0, 2, 1, 2, 3, 1 ]; + * + * Layer3D.addModelFromVertices(vertices, uvs, indicies); + * ``` + * + * You cannot add more vertices to a model than the total specified when the model was created. + * If you need to clear all vertices first, call `Model.resetVertices`. + * + * @method Phaser.GameObjects.Layer3D#addModelFromVertices + * @since 3.50.0 + * + * @param {number[]} vertices - The vertices array. Either `xy` pairs, or `xyz` if the `containsZ` parameter is `true`. + * @param {number[]} uvs - The UVs pairs array. + * @param {number[]} [normals] - Optional vertex normals array. If you don't have one, pass `null` or an empty array. + * @param {number[]} [indicies] - Optional vertex indicies array. If you don't have one, pass `null` or an empty array. + * @param {boolean} [containsZ=false] - Does the vertices data include a `z` component? + * @param {string|Phaser.Textures.Texture} [texture] - The key, or instance of the Texture the model will use to render with, as stored in the Texture Manager. + * @param {string|integer} [frame] - An optional frame from the Texture the model is rendering with. + * + * @return {Phaser.Geom.Mesh.Model} The Model instance that was created. + */ + addModelFromVertices: function (vertices, uvs, normals, indicies, containsZ, texture, frame) + { + var isIndexed = (Array.isArray(indicies) && indicies.length > 0); + + var verticesCount = (isIndexed) ? indicies.length : vertices.length; + + if (!isIndexed) + { + verticesCount /= 2; + } + + var model = this.addModel(verticesCount, texture, frame, 0, 0, 0); + + model.addVertices(vertices, uvs, normals, indicies, containsZ); + + return model; + }, + + /** + * Sets the fog values for this Layer3D, including the fog color and the near and + * far distance values. + * + * By default, fog effects all models in this layer. + * + * If you do not wish to have a fog effect, see the `disableFog` method. + * + * @method Phaser.GameObjects.Layer3D#setFog + * @since 3.50.0 + * + * @param {number} red - The red color component of the fog. A value between 0 and 1. + * @param {number} green - The green color component of the fog. A value between 0 and 1. + * @param {number} blue - The blue color component of the fog. A value between 0 and 1. + * @param {number} [near] - The 'near' value of the fog. + * @param {number} [far] - The 'far' value of the fog, beyond which objects are 'fogged' out. + * + * @return {this} This Layer3D Game Object. + */ + setFog: function (red, green, blue, near, far) + { + if (near === undefined) { near = this.fogNear; } + if (far === undefined) { far = this.fogFar; } + + this.fogColor.set(red, green, blue); + + this.fogNear = near; + this.fogFar = far; + + return this; + }, + + /** + * Disables fog for this Layer3D and all models it renders. + * + * To re-enable fog, just call `setFog` and provide new color, near and far values. + * + * @method Phaser.GameObjects.Layer3D#disableFog + * @since 3.50.0 + * + * @return {this} This Layer3D Game Object. + */ + disableFog: function () + { + this.fogFar = Infinity; + + return this; + }, + + /** + * The Layer3D update loop. + * + * @method Phaser.GameObjects.Layer3D#preUpdate + * @protected + * @since 3.50.0 + * + * @param {number} time - The current timestamp. + * @param {number} delta - The delta time, in ms, elapsed since the last frame. + */ + preUpdate: function (time, delta) + { + var width = this.width; + var height = this.height; + + var camera = this.camera; + + if (camera.dirtyProjection || width !== this._prevWidth || height !== this._prevHeight) + { + camera.updateProjectionMatrix(width, height); + + this._prevWidth = width; + this._prevHeight = height; + } + + var models = this.models; + + for (var i = 0; i < models.length; i++) + { + var model = models[i]; + + if (model.visible) + { + model.preUpdate(time, delta); + } + } + }, + + /** + * Resets all of the dirty cache values this Layer3D object uses. + * + * This is called automatically at the end of the render step. + * + * @method Phaser.GameObjects.Layer3D#resetDirtyFlags + * @protected + * @since 3.50.0 + */ + resetDirtyFlags: function () + { + this.camera.dirtyView = false; + this.camera.dirtyProjection = false; + + this.light.ambient.dirty = false; + this.light.diffuse.dirty = false; + this.light.specular.dirty = false; + + this.fogColor.dirty = false; + }, + + /** + * The destroy step for this Layer3D, which removes all models, destroys the camera and + * nulls external references. + * + * @method Phaser.GameObjects.Layer3D#preDestroy + * @private + * @since 3.50.0 + */ + preDestroy: function () + { + this.clearModels(); + + this.camera.destroy(); + this.light.destroy(); + + this.camera = null; + this.light = null; + } + +}); + +module.exports = Layer3D; diff --git a/src/gameobjects/layer3d/Layer3DCamera.js b/src/gameobjects/layer3d/Layer3DCamera.js new file mode 100644 index 000000000..33fff3c6d --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DCamera.js @@ -0,0 +1,716 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var Class = require('../../utils/Class'); +var DegToRad = require('../../math/DegToRad'); +var GetFastValue = require('../../utils/object/GetFastValue'); +var INPUT_EVENTS = require('../../input/events'); +var Matrix4 = require('../../math/Matrix4'); +var Vector3 = require('../../math/Vector3'); +var Vector4 = require('../../math/Vector4'); + +/** + * @classdesc + * The Layer3D Camera. + * + * @class Layer3DCamera + * @memberof Phaser.GameObjects + * @constructor + * @since 3.50.0 + */ +var Layer3DCamera = new Class({ + + initialize: + + function Layer3DCamera (layer, fov, x, y, z, near, far) + { + /** + * The Layer3D instance this camera belongs to. + * + * A camera can only belong to a single Layer3D instance. + * + * You should consider this property as being read-only. You cannot move a + * camera to another Layer3D by simply changing it. + * + * @name Phaser.GameObjects.Layer3DCamera#layer + * @type {Phaser.GameObjects.Layer3D} + * @since 3.50.0 + */ + this.layer = layer; + + /** + * The Scene Input Plugin, as referenced via the Layer3D parent. + * + * @name Phaser.GameObjects.Layer3DCamera#input + * @type {Phaser.Input.InputPlugin} + * @since 3.50.0 + */ + this.input = layer.scene.sys.input; + + /** + * Internal 'dirty' flag that tells the parent Layer3D if the + * view matrix of this camera needs recalculating at the next step. + * + * @name Phaser.GameObjects.Layer3DCamera#dirtyView + * @type {boolean} + * @since 3.50.0 + */ + this.dirtyView = true; + + /** + * Internal 'dirty' flag that tells the parent Layer3D if the + * projection matrix of this camera needs recalculating at the next step. + * + * @name Phaser.GameObjects.Layer3DCamera#dirtyProjection + * @type {boolean} + * @since 3.50.0 + */ + this.dirtyProjection = true; + + /** + * Internal fov value. + * + * @name Phaser.GameObjects.Layer3DCamera#_fov + * @type {number} + * @private + * @since 3.50.0 + */ + this._fov = fov; + + /** + * Internal near value. + * + * @name Phaser.GameObjects.Layer3DCamera#_near + * @type {number} + * @private + * @since 3.50.0 + */ + this._near = near; + + /** + * Internal far value. + * + * @name Phaser.GameObjects.Layer3DCamera#_far + * @type {number} + * @private + * @since 3.50.0 + */ + this._far = far; + + /** + * The aspect ratio of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#aspectRatio + * @type {number} + * @since 3.50.0 + */ + this.aspectRatio = 1; + + /** + * The position of the camera in 3D space. + * + * You can modify this vector directly, or use the `x`, `y` and `z` + * properties of this class. + * + * @name Phaser.GameObjects.Layer3DCamera#position + * @type {Phaser.Math.Vector3} + * @since 3.50.0 + */ + this.position = new Vector3(x, y, z); + + /** + * The rotation of the camera in 3D space. + * + * You can modify this vector directly, or use the `rotationX`, `rotationY` + * and `rotationZ` properties of this class. + * + * @name Phaser.GameObjects.Layer3DCamera#rotation + * @type {Phaser.Math.Vector3} + * @since 3.50.0 + */ + this.rotation = new Vector3(); + + /** + * The forward facing vector of the camera. + * + * Calculated and updated automatically when the view matrix changes. + * + * @name Phaser.GameObjects.Layer3DCamera#forward + * @type {Phaser.Math.Vector4} + * @since 3.50.0 + */ + this.forward = new Vector4(); + + /** + * The upward facing vector of the camera. + * Invert it to get the bottom vector. + * + * Calculated and updated automatically when the view matrix changes. + * + * @name Phaser.GameObjects.Layer3DCamera#up + * @type {Phaser.Math.Vector4} + * @since 3.50.0 + */ + this.up = new Vector4(); + + /** + * The right facing vector of the camera. + * Invert it to get the left vector. + * + * Calculated and updated automatically when the view matrix changes. + * + * @name Phaser.GameObjects.Layer3DCamera#right + * @type {Phaser.Math.Vector4} + * @since 3.50.0 + */ + this.right = new Vector4(); + + /** + * Internal transform matrix. + * + * Calculated and updated automatically when the camera is dirty. + * + * @name Phaser.GameObjects.Layer3DCamera#matrix + * @type {Phaser.Math.Matrix4} + * @since 3.50.0 + */ + this.matrix = new Matrix4(); + + /** + * The inverse of the transform matrix. + * + * Calculated and updated automatically when the camera is dirty. + * + * @name Phaser.GameObjects.Layer3DCamera#viewMatrix + * @type {Phaser.Math.Matrix4} + * @since 3.50.0 + */ + this.viewMatrix = new Matrix4(); + + /** + * The perspective projection matrix. + * + * Calculated and updated automatically when the camera is dirty. + * + * @name Phaser.GameObjects.Layer3DCamera#projectionMatrix + * @type {Phaser.Math.Matrix4} + * @since 3.50.0 + */ + this.projectionMatrix = new Matrix4(); + + /** + * The perspective projection matrix, multiplied by the view matrix. + * + * Calculated and updated automatically when the camera is dirty. + * + * @name Phaser.GameObjects.Layer3DCamera#viewProjectionMatrix + * @type {Phaser.Math.Matrix4} + * @since 3.50.0 + */ + this.viewProjectionMatrix = new Matrix4(); + + /** + * The movement and rotation mode of this camera. + * Either ORBIT, or FREE. + * + * @name Phaser.GameObjects.Layer3DCamera#mode + * @type {number} + * @since 3.50.0 + */ + this.mode = Layer3DCamera.MODE_ORBIT; + + /** + * How fast to rotate the camera, in degrees per delta. + * + * This value is only used after calling the `enableControls` method, + * it does not influence changing the rotation values directly. + * + * @name Phaser.GameObjects.Layer3DCamera#rotateSpeed + * @type {number} + * @since 3.50.0 + */ + this.rotateSpeed = 0.5; + + /** + * How fast to pan the camera, in units per delta. + * + * This value is only used after calling the `enableControls` method, + * it does not influence calling the pan methods directly. + * + * @name Phaser.GameObjects.Layer3DCamera#panSpeed + * @type {number} + * @since 3.50.0 + */ + this.panSpeed = 4; + + /** + * How fast to zoom the camera. + * + * This value is only used after calling the `enableControls` method, + * it does not influence calling the panZ method directly. + * + * @name Phaser.GameObjects.Layer3DCamera#zoomSpeed + * @type {number} + * @since 3.50.0 + */ + this.zoomSpeed = 3; + + this.allowPan = false; + + this.lockXAxis = false; + this.lockYAxis = false; + }, + + enableOrbitControls: function (config) + { + this.rotateSpeed = GetFastValue(config, 'rotateSpeed', this.rotateSpeed); + this.panSpeed = GetFastValue(config, 'panSpeed', this.panSpeed); + this.allowPan = GetFastValue(config, 'allowPan', this.allowPan); + this.lockXAxis = GetFastValue(config, 'lockXAxis', this.lockXAxis); + this.lockYAxis = GetFastValue(config, 'lockYAxis', this.lockYAxis); + + this.input.on(INPUT_EVENTS.POINTER_MOVE, this.pointerMoveHandler, this); + }, + + disableOrbitControls: function () + { + this.input.off(INPUT_EVENTS.POINTER_MOVE, this.pointerMoveHandler, this); + }, + + enableZoom: function (zoomSpeed) + { + if (zoomSpeed === undefined) { zoomSpeed = 3; } + + this.zoomSpeed = zoomSpeed; + + this.input.on(INPUT_EVENTS.POINTER_WHEEL, this.pointerWheelHandler, this); + }, + + disableZoom: function () + { + this.input.off(INPUT_EVENTS.POINTER_WHEEL, this.pointerWheelHandler, this); + }, + + pointerMoveHandler: function (pointer) + { + if (pointer.isDown) + { + var width = this.layer.width; + var height = this.layer.height; + + if (pointer.event.shiftKey && this.allowPan) + { + this.panX(pointer.velocity.x * (this.panSpeed / width)); + this.panY(pointer.velocity.y * (this.panSpeed / height)); + } + else + { + if (!this.lockXAxis) + { + this.rotationX -= pointer.velocity.y * (this.rotateSpeed / height); + } + + if (!this.lockYAxis) + { + this.rotationY -= pointer.velocity.x * (this.rotateSpeed / width); + } + } + } + }, + + pointerWheelHandler: function (pointer, over, deltaX, deltaY) + { + this.panZ(deltaY * (this.zoomSpeed / this.layer.height)); + }, + + /** + * Pans this camera on the x axis by the given amount. + * + * @method Phaser.GameObjects.Layer3DCamera#panX + * @since 3.50.0 + * + * @param {number} v - The amount to pan by. + */ + panX: function (v) + { + this.updateViewMatrix(); + + this.position.addScale(this.right, v); + }, + + /** + * Pans this camera on the y axis by the given amount. + * + * @method Phaser.GameObjects.Layer3DCamera#panY + * @since 3.50.0 + * + * @param {number} v - The amount to pan by. + */ + panY: function (v) + { + this.updateViewMatrix(); + + this.y += this.up.y * v; + + if (this.mode === Layer3DCamera.MODE_ORBIT) + { + // Can only move up and down the y axis in orbit mode + return; + } + + this.x += this.up.x * v; + this.z += this.up.z * v; + }, + + /** + * Pans this camera on the z axis by the given amount. + * + * @method Phaser.GameObjects.Layer3DCamera#panZ + * @since 3.50.0 + * + * @param {number} v - The amount to pan by. + */ + panZ: function (v) + { + this.updateViewMatrix(); + + if (this.mode === Layer3DCamera.MODE_ORBIT) + { + // Orbit mode translates after rotatation, so only need to set Z. The rotation will handle the rest. + this.z += v; + } + else + { + // In freemode to move forward, we move based on our forward, which is relative to our current rotation. + this.position.addScale(this.forward, v); + } + }, + + /** + * Internal method that is called by the Layer3D instance that owns this camera + * during its `render` step. If the view matrix is dirty, it is recalculated + * and then applied to the view projection matrix, ready for rendering. + * + * @method Phaser.GameObjects.Layer3DCamera#update + * @since 3.50.0 + */ + update: function () + { + if (this.dirtyView) + { + this.updateViewMatrix(); + } + + if (this.dirtyView || this.dirtyProjection) + { + this.projectionMatrix.multiplyToMat4(this.viewMatrix, this.viewProjectionMatrix); + } + }, + + /** + * Internal method that handles the update of the view transform matrix, based on the rotation + * and position of the camera. Called automatically when the camera is updated. + * + * @method Phaser.GameObjects.Layer3DCamera#updateViewMatrix + * @since 3.50.0 + */ + updateViewMatrix: function () + { + var matView = this.matrix; + + if (this.mode === Layer3DCamera.MODE_FREE) + { + matView.fromRotationXYTranslation(this.rotation, this.position, true); + } + else + { + matView.fromRotationXYTranslation(this.rotation, this.position, false); + } + + this.updateDirection(); + + this.viewMatrix.copy(matView).invert(); + }, + + /** + * Internal method that is called by the Layer3D instance that owns this camera + * during its `preUpdate` step. If the projection matrix is dirty, or the renderer + * width or height has changed, then a new projection matrix is calculated. + * + * @method Phaser.GameObjects.Layer3DCamera#updateProjectionMatrix + * @since 3.50.0 + * + * @param {number} width - The width of the renderer. + * @param {number} height - The height of the renderer. + */ + updateProjectionMatrix: function (width, height) + { + this.aspectRatio = width / height; + + this.projectionMatrix.perspective(DegToRad(this._fov), this.aspectRatio, this._near, this._far); + }, + + /** + * Internal method that sets the forward, up and right vectors from + * the view matrix. This is called automatically as part of the + * `updateViewMatrix` method. + * + * @method Phaser.GameObjects.Layer3DCamera#updateDirection + * @since 3.50.0 + */ + updateDirection: function () + { + var matView = this.matrix; + + this.forward.set(0, 0, 1, 0).transformMat4(matView); + this.up.set(0, 1, 0, 0).transformMat4(matView); + this.right.set(1, 0, 0, 0).transformMat4(matView); + }, + + /** + * The field of view, in degrees, of this camera. + * + * Limited to the range of 0 to 180. + * + * @name Phaser.GameObjects.Layer3DCamera#fov + * @type {number} + * @since 3.50.0 + */ + fov: { + + get: function () + { + return this._fov; + }, + + set: function (value) + { + if (value > 0 && value < 180) + { + this._fov = value; + this.dirtyProjection = true; + } + } + + }, + + /** + * The minimum distance the camera can see from. + * + * It's important to consider that depth buffers are not infinite and the closer + * a camera starts, the more you may encounter depth fighting issues. + * + * @name Phaser.GameObjects.Layer3DCamera#near + * @type {number} + * @since 3.50.0 + */ + near: { + + get: function () + { + return this._near; + }, + + set: function (value) + { + if (value > 0) + { + this._near = value; + this.dirtyProjection = true; + } + } + + }, + + /** + * The maximum distance the camera can see to. + * + * It's important to consider that depth buffers are not infinite and the further + * a camera ends, the more you may encounter depth fighting issues. + * + * @name Phaser.GameObjects.Layer3DCamera#far + * @type {number} + * @since 3.50.0 + */ + far: { + + get: function () + { + return this._far; + }, + + set: function (value) + { + if (value > 0) + { + this._far = value; + this.dirtyProjection = true; + } + } + + }, + + /** + * The x position of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#x + * @type {number} + * @since 3.50.0 + */ + x: { + + get: function () + { + return this.position.x; + }, + + set: function (value) + { + this.position.x = value; + this.dirtyView = true; + } + + }, + + /** + * The y position of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#y + * @type {number} + * @since 3.50.0 + */ + y: { + + get: function () + { + return this.position.y; + }, + + set: function (value) + { + this.position.y = value; + this.dirtyView = true; + } + + }, + + /** + * The z position of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#z + * @type {number} + * @since 3.50.0 + */ + z: { + + get: function () + { + return this.position.z; + }, + + set: function (value) + { + this.position.z = value; + this.dirtyView = true; + } + + }, + + /** + * The x axis rotation, in radians, of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#rotationX + * @type {number} + * @since 3.50.0 + */ + rotationX: { + + get: function () + { + return this.rotation.x; + }, + + set: function (value) + { + this.rotation.x = value; + this.dirtyView = true; + } + + }, + + /** + * The y axis rotation, in radians, of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#rotationY + * @type {number} + * @since 3.50.0 + */ + rotationY: { + + get: function () + { + return this.rotation.y; + }, + + set: function (value) + { + this.rotation.y = value; + this.dirtyView = true; + } + + }, + + /** + * The z axis rotation, in radians, of the camera. + * + * @name Phaser.GameObjects.Layer3DCamera#rotationZ + * @type {number} + * @since 3.50.0 + */ + rotationZ: { + + get: function () + { + return this.rotation.z; + }, + + set: function (value) + { + this.rotation.z = value; + this.dirtyView = true; + } + + }, + + /** + * Destroy handler for this camera. + * + * @method Phaser.GameObjects.Layer3DCamera#destroy + * @since 3.50.0 + */ + destroy: function () + { + this.layer = null; + this.position = null; + this.rotation = null; + this.forward = null; + this.up = null; + this.right = null; + this.matrix = null; + this.viewMatrix = null; + this.projectionMatrix = null; + this.viewProjectionMatrix = null; + } + +}); + +// Allows free movement of position and rotation +Layer3DCamera.MODE_FREE = 0; + +// Movement is locked to rotate around the origin +Layer3DCamera.MODE_ORBIT = 1; + +module.exports = Layer3DCamera; diff --git a/src/gameobjects/layer3d/Layer3DCanvasRenderer.js b/src/gameobjects/layer3d/Layer3DCanvasRenderer.js new file mode 100644 index 000000000..f692ee998 --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DCanvasRenderer.js @@ -0,0 +1,22 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +/** + * This is a stub function for Layer3D.Render. There is no Canvas renderer for Layer3D objects. + * + * @method Phaser.GameObjects.Layer3D#renderCanvas + * @since 3.50.0 + * @private + * + * @param {Phaser.Renderer.Canvas.CanvasRenderer} renderer - A reference to the current active Canvas renderer. + * @param {Phaser.GameObjects.Layer3D} src - The Game Object being rendered in this call. + * @param {Phaser.Cameras.Scene2D.Camera} camera - The Camera that is rendering the Game Object. + */ +var Layer3DCanvasRenderer = function () +{ +}; + +module.exports = Layer3DCanvasRenderer; diff --git a/src/gameobjects/layer3d/Layer3DCreator.js b/src/gameobjects/layer3d/Layer3DCreator.js new file mode 100644 index 000000000..547844caf --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DCreator.js @@ -0,0 +1,40 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var BuildGameObject = require('../BuildGameObject'); +var GameObjectCreator = require('../GameObjectCreator'); +var Layer3D = require('./Layer3D'); + +/** + * Creates a new Layer3D Game Object and returns it. + * + * Note: This method will only be available if the Layer3D Game Object and WebGL support have been built into Phaser. + * + * @method Phaser.GameObjects.GameObjectCreator#layer3d + * @since 3.50.0 + * + * @param {object} config - The configuration object this Game Object will use to create itself. + * @param {boolean} [addToScene] - Add this Game Object to the Scene after creating it? If set this argument overrides the `add` property in the config object. + * + * @return {Phaser.GameObjects.Layer3D} The Game Object that was created. + */ +GameObjectCreator.register('layer3d', function (config, addToScene) +{ + if (config === undefined) { config = {}; } + + var layer = new Layer3D(this.scene, 0, 0); + + if (addToScene !== undefined) + { + config.add = addToScene; + } + + BuildGameObject(this.scene, layer, config); + + return layer; +}); + +// When registering a factory function 'this' refers to the GameObjectCreator context. diff --git a/src/gameobjects/layer3d/Layer3DFactory.js b/src/gameobjects/layer3d/Layer3DFactory.js new file mode 100644 index 000000000..0c9aabd15 --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DFactory.js @@ -0,0 +1,38 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var Layer3D = require('./Layer3D'); +var GameObjectFactory = require('../GameObjectFactory'); + +/** + * Creates a new Layer3D Game Object and adds it to the Scene. + * + * Note: This method will only be available if the Layer3D Game Object and WebGL support have been built into Phaser. + * + * @method Phaser.GameObjects.GameObjectFactory#layer3d + * @webglOnly + * @since 3.50.0 + * + * @param {number} [x] - The horizontal position of this Game Object in the world. + * @param {number} [y] - The vertical position of this Game Object in the world. + * + * @return {Phaser.GameObjects.Layer3D} The Game Object that was created. + */ +if (typeof WEBGL_RENDERER) +{ + GameObjectFactory.register('layer3d', function (x, y) + { + return this.displayList.add(new Layer3D(this.scene, x, y)); + }); +} + +// When registering a factory function 'this' refers to the GameObjectFactory context. +// +// There are several properties available to use: +// +// this.scene - a reference to the Scene that owns the GameObjectFactory +// this.displayList - a reference to the Display List the Scene owns +// this.updateList - a reference to the Update List the Scene owns diff --git a/src/gameobjects/layer3d/Layer3DLight.js b/src/gameobjects/layer3d/Layer3DLight.js new file mode 100644 index 000000000..129a16810 --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DLight.js @@ -0,0 +1,295 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var Class = require('../../utils/Class'); +var RGB = require('../../display/RGB'); +var Vector3 = require('../../math/Vector3'); + +/** + * @classdesc + * A Layer3D Light. + * + * @class Layer3DLight + * @memberof Phaser.GameObjects + * @constructor + * @since 3.50.0 + */ +var Layer3DLight = new Class({ + + initialize: + + function Layer3DLight (layer, x, y, z) + { + /** + * The Layer3D instance this light belongs to. + * + * A light can only belong to a single Layer3D instance. + * + * You should consider this property as being read-only. You cannot move a + * light to another Layer3D by simply changing it. + * + * @name Phaser.GameObjects.Layer3DLight#layer + * @type {Phaser.GameObjects.Layer3D} + * @since 3.50.0 + */ + this.layer = layer; + + /** + * The position of the light in 3D space. + * + * You can modify this vector directly, or use the `x`, `y` and `z` + * properties of this class. + * + * @name Phaser.GameObjects.Layer3DLight#position + * @type {Phaser.Math.Vector3} + * @since 3.50.0 + */ + this.position = new Vector3(x, y, z); + + /** + * The ambient color of the light. + * + * The default ambient color is 1, 1, 1. + * + * You can modify the properties of this RGB object directly, or call + * the `setAmbient` method of this class. + * + * The values in this object are used by the `uLightAmbient` shader uniform. + * + * @name Phaser.GameObjects.Layer3DLight#ambient + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.ambient = new RGB(1, 1, 1); + + /** + * The diffuse color of the light. + * + * The default diffuse color is 1, 1, 1. + * + * You can modify the properties of this RGB object directly, or call + * the `setDiffuse` method of this class. + * + * The values in this object are used by the `uLightDiffuse` shader uniform. + * + * @name Phaser.GameObjects.Layer3DLight#diffuse + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.diffuse = new RGB(1, 1, 1); + + /** + * The specular color of the light. + * + * The default specular color is 1, 1, 1. + * + * You can modify the properties of this RGB object directly, or call + * the `setSpecular` method of this class. + * + * The values in this object are used by the `uLightSpecular` shader uniform. + * + * @name Phaser.GameObjects.Layer3DLight#specular + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.specular = new RGB(1, 1, 1); + + /** + * Internal dirty cache array. + * + * @name Phaser.GameObjects.Layer3DLight#dirtyCache + * @type {number[]} + * @private + * @since 3.50.0 + */ + this.dirtyCache = [ 0, 0, 0 ]; + }, + + /** + * Checks if the position of this light is dirty. + * + * Called internally by the Mesh Pipeline `onBind` method and if dirty + * is used to set the `uLightPosition` uniform. + * + * @method Phaser.GameObjects.Layer3DLight#isDirty + * @since 3.50.0 + * + * @return {boolean} `true` if this light is dirty, otherwise `false`. + */ + isDirty: function () + { + var position = this.position; + var dirtyCache = this.dirtyCache; + + var x = position.x; + var y = position.y; + var z = position.z; + + var xCached = dirtyCache[0]; + var yCached = dirtyCache[1]; + var zCached = dirtyCache[2]; + + dirtyCache[0] = x; + dirtyCache[1] = y; + dirtyCache[2] = z; + + return (xCached !== x || yCached !== y || zCached !== z); + }, + + /** + * Sets the position of this light. + * + * @method Phaser.GameObjects.Layer3DLight#setPosition + * @since 3.50.0 + * + * @param {number} x - The x position of this light. + * @param {number} y - The y position of this light. + * @param {number} z - The z position of this light. + * + * @return {this} This Layer3DLight instance. + */ + setPosition: function (x, y, z) + { + this.position.set(x, y, z); + + return this; + }, + + /** + * Sets the ambient color of this light. + * + * @method Phaser.GameObjects.Layer3DLight#setAmbient + * @since 3.50.0 + * + * @param {number} r - The red color value. Between 0 and 1. + * @param {number} g - The green color value. Between 0 and 1. + * @param {number} b - The blue color value. Between 0 and 1. + * + * @return {this} This Layer3DLight instance. + */ + setAmbient: function (r, g, b) + { + this.ambient.set(r, g, b); + + return this; + }, + + /** + * Sets the diffuse color of this light. + * + * @method Phaser.GameObjects.Layer3DLight#setDiffuse + * @since 3.50.0 + * + * @param {number} r - The red color value. Between 0 and 1. + * @param {number} g - The green color value. Between 0 and 1. + * @param {number} b - The blue color value. Between 0 and 1. + * + * @return {this} This Layer3DLight instance. + */ + setDiffuse: function (r, g, b) + { + this.diffuse.set(r, g, b); + + return this; + }, + + /** + * Sets the specular color of this light. + * + * @method Phaser.GameObjects.Layer3DLight#setSpecular + * @since 3.50.0 + * + * @param {number} r - The red color value. Between 0 and 1. + * @param {number} g - The green color value. Between 0 and 1. + * @param {number} b - The blue color value. Between 0 and 1. + * + * @return {this} This Layer3DLight instance. + */ + setSpecular: function (r, g, b) + { + this.specular.set(r, g, b); + + return this; + }, + + /** + * The x position of the light. + * + * @name Phaser.GameObjects.Layer3DLight#x + * @type {number} + * @since 3.50.0 + */ + x: { + + get: function () + { + return this.position.x; + }, + + set: function (value) + { + this.position.x = value; + } + + }, + + /** + * The y position of the light. + * + * @name Phaser.GameObjects.Layer3DLight#y + * @type {number} + * @since 3.50.0 + */ + y: { + + get: function () + { + return this.position.y; + }, + + set: function (value) + { + this.position.y = value; + } + + }, + + /** + * The z position of the light. + * + * @name Phaser.GameObjects.Layer3DLight#z + * @type {number} + * @since 3.50.0 + */ + z: { + + get: function () + { + return this.position.z; + }, + + set: function (value) + { + this.position.z = value; + } + + }, + + /** + * Destroy handler for this light. + * + * @method Phaser.GameObjects.Layer3DLight#destroy + * @since 3.50.0 + */ + destroy: function () + { + this.layer = null; + this.position = null; + } + +}); + +module.exports = Layer3DLight; diff --git a/src/gameobjects/layer3d/Layer3DRender.js b/src/gameobjects/layer3d/Layer3DRender.js new file mode 100644 index 000000000..045af538d --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DRender.js @@ -0,0 +1,25 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var renderWebGL = require('../../utils/NOOP'); +var renderCanvas = require('../../utils/NOOP'); + +if (typeof WEBGL_RENDERER) +{ + renderWebGL = require('./Layer3DWebGLRenderer'); +} + +if (typeof CANVAS_RENDERER) +{ + renderCanvas = require('./Layer3DCanvasRenderer'); +} + +module.exports = { + + renderWebGL: renderWebGL, + renderCanvas: renderCanvas + +}; diff --git a/src/gameobjects/layer3d/Layer3DWebGLRenderer.js b/src/gameobjects/layer3d/Layer3DWebGLRenderer.js new file mode 100644 index 000000000..ee682a02a --- /dev/null +++ b/src/gameobjects/layer3d/Layer3DWebGLRenderer.js @@ -0,0 +1,52 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +/** + * Renders this Game Object with the WebGL Renderer to the given Camera. + * The object will not render if any of its renderFlags are set or it is being actively filtered out by the Camera. + * This method should not be called directly. It is a utility function of the Render module. + * + * @method Phaser.GameObjects.Layer3D#renderWebGL + * @since 3.50.0 + * @private + * + * @param {Phaser.Renderer.WebGL.WebGLRenderer} renderer - A reference to the current active WebGL renderer. + * @param {Phaser.GameObjects.Layer3D} src - The Game Object being rendered in this call. + * @param {Phaser.Cameras.Scene2D.Camera} camera - The Camera that is rendering the Game Object. + * @param {Phaser.GameObjects.Components.TransformMatrix} parentMatrix - This transform matrix is defined if the game object is nested + */ +var Layer3DWebGLRenderer = function (renderer, src) +{ + var models = src.models; + var totalModels = models.length; + + if (totalModels === 0) + { + return; + } + + renderer.pipelines.clear(); + + src.camera.update(); + + var pipeline = renderer.pipelines.set(src.pipeline, src); + + for (var m = 0; m < totalModels; m++) + { + var model = models[m]; + + if (model.visible && model.vertexCount > 0) + { + pipeline.drawModel(src, model); + } + } + + src.resetDirtyFlags(); + + renderer.pipelines.rebind(); +}; + +module.exports = Layer3DWebGLRenderer; diff --git a/src/geom/mesh/Model.js b/src/geom/mesh/Model.js new file mode 100644 index 000000000..12f704fa4 --- /dev/null +++ b/src/geom/mesh/Model.js @@ -0,0 +1,918 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var AnimationState = require('../../animations/AnimationState'); +var Class = require('../../utils/Class'); +var Components = require('../../gameobjects/components'); +var Matrix4 = require('../../math/Matrix4'); +var Quaternion = require('../../math/Quaternion'); +var RGB = require('../../layer3d/math/RGB'); +var Vector3 = require('../../math/Vector3'); + +/** + * @classdesc + * A 3D Model. + * + * @class Model + * @memberof Phaser.Geom.Mesh + * @constructor + * @since 3.50.0 + * + * @param {Phaser.GameObjects.Layer3D} layer - A reference to the Layer3D instance that this model belongs to. + * @param {number} verticesCount - The total number of vertices this model can contain. + * @param {string|Phaser.Textures.Texture} [texture] - The key, or instance of the Texture this model will use to render with, as stored in the Texture Manager. + * @param {string|integer} [frame] - An optional frame from the Texture this model is rendering with. Ensure your UV data also matches this frame. + * @param {number} [x=0] - The x position of the Model. + * @param {number} [y=0] - The y position of the Model. + * @param {number} [z=0] - The z position of the Model. + */ +var Model = new Class({ + + Mixins: [ + Components.AlphaSingle, + Components.Size, + Components.Texture, + Components.Visible + ], + + initialize: + + function Model (layer, verticesCount, texture, frame, x, y, z) + { + if (x === undefined) { x = 0; } + if (y === undefined) { y = 0; } + if (z === undefined) { z = 0; } + + /** + * The Layer3D instance this model belongs to. + * + * A model can only belong to a single Layer3D instance. + * + * You should consider this property as being read-only. You cannot move a + * model to another Layer3D by simply changing it. + * + * @name Phaser.Geom.Mesh.Model#layer + * @type {Phaser.GameObjects.Layer3D} + * @since 3.50.0 + */ + this.layer = layer; + + /** + * A reference to the Scene to which this the Layer3D Object which owns this model belongs. + * + * You should consider this property as being read-only. You cannot move a + * Game Object to another Scene by simply changing it. + * + * @name Phaser.Geom.Mesh.Model#scene + * @type {Phaser.Scene} + * @since 3.50.0 + */ + this.scene = layer.scene; + + /** + * The Animation State of this Model. + * + * @name Phaser.Geom.Mesh.Model#anims + * @type {Phaser.Animation.AnimationState} + * @since 3.50.0 + */ + this.anims = new AnimationState(this); + + /** + * The size of a single vertex, in bytes. + * + * The total of all 8 attributes * bytes size. + * + * @name Phaser.Geom.Mesh.Model#vertexSize + * @type {number} + * @since 3.50.0 + */ + this.vertexSize = 32; + + /** + * The total number of vertices the ArrayBuffer in this model can hold. + * + * @name Phaser.Geom.Mesh.Model#maxVertexCount + * @type {number} + * @since 3.50.0 + */ + this.maxVertexCount = verticesCount; + + /** + * The total number of vertices currently added to this model. + * + * @name Phaser.Geom.Mesh.Model#vertexCount + * @type {number} + * @since 3.50.0 + */ + this.vertexCount = 0; + + /** + * An ArrayBuffer that contains the GPU byte data for this model. + * + * The size of the buffer is set to `verticesCount * vertexSize` when + * this model is created and cannot be changed without resetting the vertices. + * + * @name Phaser.Geom.Mesh.Model#vertexData + * @type {ArrayBuffer} + * @since 3.50.0 + */ + this.vertexData; + + /** + * A Float32 View into the Array Buffer. + * + * @name Phaser.Geom.Mesh.Model#vertexViewF32 + * @type {Float32Array} + * @since 3.50.0 + */ + this.vertexViewF32; + + /** + * A Vector3 containing the position of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#position + * @type {Phaser.Math.Vector3} + * @since 3.50.0 + */ + this.position = new Vector3(x, y, z); + + /** + * A Vector3 containing the scale of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#scale + * @type {Phaser.Math.Vector3} + * @since 3.50.0 + */ + this.scale = new Vector3(1, 1, 1); + + /** + * A Quaternion containing the rotation of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#position + * @type {Phaser.Math.Quaternion} + * @since 3.50.0 + */ + this.rotation = new Quaternion(); + + /** + * An RGB object containing the ambient material color of this model. + * + * You can adjust the ambient material color by calling the methods + * on this object and changing its properties. + * + * Remember that all color values should be specified in the range + * of 0 to 1. + * + * @name Phaser.Geom.Mesh.Model#ambient + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.ambient = new RGB(1, 1, 1); + + /** + * An RGB object containing the diffuse material color of this model. + * + * You can adjust the diffuse material color by calling the methods + * on this object and changing its properties. + * + * Remember that all color values should be specified in the range + * of 0 to 1. + * + * @name Phaser.Geom.Mesh.Model#diffuse + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.diffuse = new RGB(1, 1, 1); + + /** + * An RGB object containing the specular material color of this model. + * + * You can adjust the specular material color by calling the methods + * on this object and changing its properties. + * + * Remember that all color values should be specified in the range + * of 0 to 1. + * + * @name Phaser.Geom.Mesh.Model#specular + * @type {Phaser.Display.RGB} + * @since 3.50.0 + */ + this.specular = new RGB(1, 1, 1); + + /** + * The material shine value of this model. + * + * Default to 0.25. Keep this value in the range 0 to 1. + * + * @name Phaser.Geom.Mesh.Model#ambient + * @type {number} + * @default 0.25 + * @since 3.50.0 + */ + this.shine = 0.25; + + /** + * A Matrix4 containing the transformed normal values for this model. + * + * You should consider this Matrix as being read-only. Its values are + * repopulated during `Model.preUpdate` as required. + * + * @name Phaser.Geom.Mesh.Model#normalMatrix + * @type {Phaser.Math.Matrix4} + * @since 3.50.0 + */ + this.normalMatrix = new Matrix4(); + + /** + * A Matrix4 containing the transform matrix for this model. + * + * You should consider this Matrix as being read-only. Its values are + * repopulated during `Model.preUpdate` as required. + * + * @name Phaser.Geom.Mesh.Model#transformMatrix + * @type {Phaser.Math.Matrix4} + * @since 3.50.0 + */ + this.transformMatrix = new Matrix4(); + + /** + * The culling mode used by this Model during rendering. + * + * Specifies whether front or back facing polygons are candidates + * for culling. The default value is `gl.BACK`. Possible values are: + * + * `gl.FRONT` (1028) + * `gl.BACK` (1029) + * `gl.FRONT_AND_BACK` (1032) + * + * @name Phaser.Geom.Mesh.Model#cullMode + * @type {GLenum} + * @since 3.50.0 + */ + this.cullMode = 1029; + + /** + * An internal cache, used to compare position, rotation, scale and verts data + * each frame, to avoid math calculates in `preUpdate`. + * + * cache structure = position | rotation | scale | verts count + * + * @name Phaser.Geom.Mesh.Model#dirtyCache + * @type {number[]} + * @private + * @since 3.50.0 + */ + this.dirtyCache = [ x, y, z, 0, 0, 0, 1, 1, 1, 1, 0 ]; + + if (!texture) + { + texture = this.scene.sys.textures.get('__WHITE'); + } + + this.setTexture(texture, frame); + + this.setSizeToFrame(); + + this.resetVertices(verticesCount); + }, + + /** + * Calls each of the listeners registered for a given event. + * + * This is a proxy for the Layer3D `emit` method. + * + * @method Phaser.Geom.Mesh.Model#emit + * @since 3.50.0 + * + * @param {(string|symbol)} event - The event name. + * @param {...*} [args] - Additional arguments that will be passed to the event handler. + * + * @return {boolean} `true` if the event had listeners, else `false`. + */ + emit: function () + { + return this.layer.emit.call(arguments); + }, + + /** + * Checks all of the current model values against the `dirtyCache` to see if the + * normal and transform matrices need updating. + * + * @method Phaser.Geom.Mesh.Model#isDirty + * @since 3.50.0 + * + * @return {boolean} Returns `true` if any of the model values are dirty, otherwise `false`. + */ + isDirty: function () + { + var position = this.position; + var rotation = this.rotation; + var scale = this.scale; + + var dirtyCache = this.dirtyCache; + + var px = position.x; + var py = position.y; + var pz = position.z; + + var rx = rotation.x; + var ry = rotation.y; + var rz = rotation.z; + var rw = rotation.w; + + var sx = scale.x; + var sy = scale.y; + var sz = scale.z; + + var vertices = this.vertexCount; + + var pxCached = dirtyCache[0]; + var pyCached = dirtyCache[1]; + var pzCached = dirtyCache[2]; + + var rxCached = dirtyCache[3]; + var ryCached = dirtyCache[4]; + var rzCached = dirtyCache[5]; + var rwCached = dirtyCache[6]; + + var sxCached = dirtyCache[7]; + var syCached = dirtyCache[8]; + var szCached = dirtyCache[9]; + + var vCached = dirtyCache[10]; + + dirtyCache[0] = px; + dirtyCache[1] = py; + dirtyCache[2] = pz; + + dirtyCache[3] = rx; + dirtyCache[4] = ry; + dirtyCache[5] = rz; + dirtyCache[6] = rw; + + dirtyCache[7] = sx; + dirtyCache[8] = sy; + dirtyCache[9] = sz; + + dirtyCache[10] = vertices; + + return ( + pxCached !== px || pyCached !== py || pzCached !== pz || + rxCached !== rx || ryCached !== ry || rzCached !== rz || rwCached !== rw || + sxCached !== sx || syCached !== sy || szCached !== sz || + vCached !== vertices + ); + }, + + /** + * Internal update handler. Advances any animations that are set on the model and, + * if the model data is dirty, recalculates the transform and normal matrices. + * + * This method is called automatically by the `Layer3D` to which this model belongs. + * + * @method Phaser.Geom.Mesh.Model#preUpdate + * @since 3.50.0 + * + * @param {number} time - The current timestamp. + * @param {number} delta - The delta time, in ms, elapsed since the last frame. + */ + preUpdate: function (time, delta) + { + this.anims.update(time, delta); + + // If the model isn't dirty we can bail out and save lots of math + if (this.isDirty()) + { + var normalMatrix = this.normalMatrix; + var transformMatrix = this.transformMatrix; + + // TODO - Merge scale into this op + transformMatrix.fromRotationTranslation(this.rotation, this.position); + transformMatrix.scale(this.scale); + + normalMatrix.copy(transformMatrix); + normalMatrix.invert(); + normalMatrix.transpose(); + } + }, + + /** + * Returns the total number of Faces _currently added_ to this model. + * + * Models in Phaser 3 must always be triangulated, so this value is the same as + * `vertexCount / 3`. + * + * @method Phaser.Geom.Mesh.Model#getFaceCount + * @since 3.50.0 + * + * @return {number} The number of Faces in this Model. + */ + getFaceCount: function () + { + return this.vertexCount / 3; + }, + + /** + * Gets the Vertex at the given offset from this models data. + * + * Be aware that the returned Vertex is untranslated, so will need transforming if you wish + * to use its coordinates in world space. + * + * @method Phaser.Geom.Mesh.Model#getVertex + * @since 3.50.0 + * + * @param {number} index - The index of the vertex to get. Cannot be negative, or exceed `Model.vertexCount`. + * + * @return {Phaser.Types.GameObjects.Vertex} A Vertex object. + */ + getVertex: function (index) + { + var vertexViewF32 = this.vertexViewF32; + + // 8 = attribute count (number of items added into the view below) + var vertexOffset = (index * 8) - 1; + + var x = vertexViewF32[++vertexOffset]; + var y = vertexViewF32[++vertexOffset]; + var z = vertexViewF32[++vertexOffset]; + var normalX = vertexViewF32[++vertexOffset]; + var normalY = vertexViewF32[++vertexOffset]; + var normalZ = vertexViewF32[++vertexOffset]; + var u = vertexViewF32[++vertexOffset]; + var v = vertexViewF32[++vertexOffset]; + + return { x: x, y: y, z: z, u: u, v: v, normalX: normalX, normalY: normalY, normalZ: normalZ, alpha: 1 }; + }, + + /** + * Returns the Face at the given index in this model. + * + * A face comprises of 3 vertices. + * + * Be aware that the Face vertices are untranslated, so will need transforming if you wish + * to use their coordinates in world space. + * + * @method Phaser.Geom.Mesh.Model#getFace + * @since 3.50.0 + * + * @param {number} index - The index of the Face to get. Make sure the index is in range. + * + * @return {Phaser.Types.GameObjects.Face} The Face at the given index. + */ + getFace: function (index) + { + var offset = index * 3; + + var v1 = this.getVertex(offset); + var v2 = this.getVertex(offset + 1); + var v3 = this.getVertex(offset + 2); + var ccw = (v2.x - v1.x) * (v3.y - v1.y) - (v2.y - v1.y) * (v3.x - v1.x) >= 0; + + return { vertex1: v1, vertex2: v2, vertex3: 3, isCounterClockwise: ccw }; + }, + + /** + * Resets the data in this model, clearing the `vertexData` ArrayBuffer and + * setting it to the new max count given. + * + * @method Phaser.Geom.Mesh.Model#resetVertices + * @since 3.50.0 + * + * @param {number} verticesCount - The total number of vertices this model can contain. + */ + resetVertices: function (verticesCount) + { + this.vertexData = new ArrayBuffer(verticesCount * this.vertexSize); + this.vertexViewF32 = new Float32Array(this.vertexData); + this.vertexCount = 0; + this.maxVertexCount = verticesCount; + + return this; + }, + + /** + * Updates all values of the vertex at the given index. + * + * Ensure that the index is in range. + * + * @method Phaser.Geom.Mesh.Model#updateVertex + * @since 3.50.0 + * + * @param {number} index - The index of the vertex to update. + * @param {number} x - The x position of the vertex. + * @param {number} y - The y position of the vertex. + * @param {number} z - The z position of the vertex. + * @param {number} u - The UV u coordinate of the vertex. + * @param {number} v - The UV v coordinate of the vertex. + * @param {number} normalX - The x normal of the vertex. + * @param {number} normalY - The y normal of the vertex. + * @param {number} normalZ - The z normal of the vertex. + */ + updateVertex: function (index, x, y, z, u, v, normalX, normalY, normalZ) + { + var vertexViewF32 = this.vertexViewF32; + + // 8 = attribute count + var vertexOffset = (index * 8) - 1; + + vertexViewF32[++vertexOffset] = x; + vertexViewF32[++vertexOffset] = y; + vertexViewF32[++vertexOffset] = z; + vertexViewF32[++vertexOffset] = normalX; + vertexViewF32[++vertexOffset] = normalY; + vertexViewF32[++vertexOffset] = normalZ; + vertexViewF32[++vertexOffset] = u; + vertexViewF32[++vertexOffset] = v; + }, + + /** + * Adds a new vertex to this model and increments the `vertexCount` by one. + * + * You cannot add more vertices to this model than the total specified when the model was created. + * If you need to clear all vertices first, call `Model.resetVertices`. + * + * @method Phaser.Geom.Mesh.Model#addVertex + * @since 3.50.0 + * + * @param {number} x - The x position of the vertex. + * @param {number} y - The y position of the vertex. + * @param {number} z - The z position of the vertex. + * @param {number} u - The UV u coordinate of the vertex. + * @param {number} v - The UV v coordinate of the vertex. + * @param {number} normalX - The x normal of the vertex. + * @param {number} normalY - The y normal of the vertex. + * @param {number} normalZ - The z normal of the vertex. + */ + addVertex: function (x, y, z, u, v, normalX, normalY, normalZ) + { + if (this.vertexCount < this.maxVertexCount) + { + this.updateVertex(this.vertexCount, x, y, z, u, v, normalX, normalY, normalZ); + + this.vertexCount++; + } + }, + + /** + * Adds vertices to this model by parsing the given arrays. + * + * This method will take vertex data in one of two formats, based on the `containsZ` parameter. + * + * If your vertex data are `x`, `y` pairs, then `containsZ` should be `false` (this is the default) + * + * If your vertex data is groups of `x`, `y` and `z` values, then the `containsZ` parameter must be true. + * + * The `uvs` parameter is a numeric array consisting of `u` and `v` pairs. + * The `normals` parameter is a numeric array consisting of `x`, `y` vertex normal values and, if `containsZ` is true, `z` values as well. + * The `indicies` parameter is an optional array that, if given, is an indexed list of vertices to be added. + * + * The following example will create a 256 x 256 sized quad using an index array: + * + * ```javascript + * const vertices = [ + * -128, 128, + * 128, 128, + * -128, -128, + * 128, -128 + * ]; + * + * const uvs = [ + * 0, 1, + * 1, 1, + * 0, 0, + * 1, 0 + * ]; + * + * const indices = [ 0, 2, 1, 2, 3, 1 ]; + * + * Model.addVertices(vertices, uvs, indicies); + * ``` + * + * You cannot add more vertices to this model than the total specified when the model was created. + * If you need to clear all vertices first, call `Model.resetVertices`. + * + * @method Phaser.Geom.Mesh.Model#addVertices + * @since 3.50.0 + * + * @param {number[]} vertices - The vertices array. Either `xy` pairs, or `xyz` if the `containsZ` parameter is `true`. + * @param {number[]} uvs - The UVs pairs array. + * @param {number[]} [normals] - Optional vertex normals array. If you don't have one, pass `null` or an empty array. + * @param {number[]} [indicies] - Optional vertex indicies array. If you don't have one, pass `null` or an empty array. + * @param {boolean} [containsZ=false] - Does the vertices data include a `z` component? + */ + addVertices: function (vertices, uvs, normals, indicies, containsZ) + { + if (containsZ === undefined) { containsZ = false; } + + if (vertices.length !== uvs.length) + { + throw new Error('Model vertices and uv count not equal'); + } + + var i; + var x; + var y; + var z; + var u; + var v; + var normalX; + var normalY; + var normalZ; + var iInc = (containsZ) ? 3 : 2; + + if (Array.isArray(indicies) && indicies.length > 0) + { + for (i = 0; i < indicies.length; i++) + { + var index = indicies[i] * iInc; + + x = vertices[index]; + y = vertices[index + 1]; + z = (containsZ) ? vertices[index + 2] : 0; + u = uvs[index]; + v = uvs[index + 1]; + normalX = 0; + normalY = 0; + normalZ = 0; + + if (normals) + { + normalX = normals[index]; + normalY = normals[index + 1]; + normalZ = (containsZ) ? normals[index + 2] : 0; + } + + this.addVertex( + x, y, z, + u, v, + normalX, normalY, normalZ + ); + } + } + else + { + for (i = 0; i < vertices.length; i += iInc) + { + x = vertices[i]; + y = vertices[i + 1]; + z = (containsZ) ? vertices[i + 2] : 0; + u = uvs[i]; + v = uvs[i + 1]; + normalX = 0; + normalY = 0; + normalZ = 0; + + if (normals) + { + normalX = normals[i]; + normalY = normals[i + 1]; + normalZ = (containsZ) ? normals[i + 2] : 0; + } + + this.addVertex( + x, y, z, + u, v, + normalX, normalY, normalZ + ); + } + } + }, + + /** + * Rotates this model along the x axis by the given amount. + * + * This method works by calling the `rotateX` method of the `rotation` quaternion of this model. + * + * @method Phaser.Geom.Mesh.Model#rotateX + * @since 3.50.0 + * + * @param {number} rad - The amount, in radians, to rotate the model by. + * + * @return {this} This model instance. + */ + rotateX: function (rad) + { + this.rotation.rotateX(rad); + + return this; + }, + + /** + * Rotates this model along the y axis by the given amount. + * + * This method works by calling the `rotateY` method of the `rotation` quaternion of this model. + * + * @method Phaser.Geom.Mesh.Model#rotateY + * @since 3.50.0 + * + * @param {number} rad - The amount, in radians, to rotate the model by. + * + * @return {this} This model instance. + */ + rotateY: function (rad) + { + this.rotation.rotateY(rad); + + return this; + }, + + /** + * Rotates this model along the z axis by the given amount. + * + * This method works by calling the `rotateZ` method of the `rotation` quaternion of this model. + * + * @method Phaser.Geom.Mesh.Model#rotateZ + * @since 3.50.0 + * + * @param {number} rad - The amount, in radians, to rotate the model by. + * + * @return {this} This model instance. + */ + rotateZ: function (rad) + { + this.rotation.rotateZ(rad); + + return this; + }, + + setPosition: function (x, y, z) + { + this.position.set(x, y, z); + + return this; + }, + + setScale: function (x, y, z) + { + this.scale.set(x, y, z); + + return this; + }, + + /** + * The x position of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#x + * @type {number} + * @since 3.50.0 + */ + x: { + + get: function () + { + return this.position.x; + }, + + set: function (value) + { + this.position.x = value; + } + + }, + + /** + * The y position of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#y + * @type {number} + * @since 3.50.0 + */ + y: { + + get: function () + { + return this.position.y; + }, + + set: function (value) + { + this.position.y = value; + } + + }, + + /** + * The z position of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#z + * @type {number} + * @since 3.50.0 + */ + z: { + + get: function () + { + return this.position.z; + }, + + set: function (value) + { + this.position.z = value; + } + + }, + + /** + * The x scale of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#scaleX + * @type {number} + * @since 3.50.0 + */ + scaleX: { + + get: function () + { + return this.scale.x; + }, + + set: function (value) + { + this.scale.x = value; + } + + }, + + /** + * The y scale of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#scaleY + * @type {number} + * @since 3.50.0 + */ + scaleY: { + + get: function () + { + return this.scale.y; + }, + + set: function (value) + { + this.scale.y = value; + } + + }, + + /** + * The z scale of this model in 3D space. + * + * @name Phaser.Geom.Mesh.Model#scaleZ + * @type {number} + * @since 3.50.0 + */ + scaleZ: { + + get: function () + { + return this.scale.z; + }, + + set: function (value) + { + this.scale.z = value; + } + + }, + + /** + * Destroys this Model instance, all of its vertex data and references. + * + * Calling this method will not remove it from any parent Layer3D, so be sure to do that first, + * prior to calling `destroy`. + * + * If a Layer3D object is destroyed, this is the method that is called on all of its models. + * + * @method Phaser.Geom.Mesh.Model#destroy + * @since 3.50.0 + */ + destroy: function () + { + this.anims.destroy(); + + this.layer = null; + this.scene = null; + this.anims = null; + + this.vertexData = null; + this.vertexViewF32 = null; + + this.position = null; + this.scale = null; + this.rotation = null; + + this.ambient = null; + this.diffuse = null; + this.specular = null; + + this.normalMatrix = null; + this.transformMatrix = null; + } + +}); + +module.exports = Model; diff --git a/src/geom/mesh/Vertex.js b/src/geom/mesh/Vertex.js index 34b381931..881ba3335 100644 --- a/src/geom/mesh/Vertex.js +++ b/src/geom/mesh/Vertex.js @@ -70,6 +70,33 @@ var Vertex = new Class({ */ this.vz = 0; + /** + * The projected x coordinate of this vertex. + * + * @name Phaser.Geom.Mesh.Vertex#vx + * @type {number} + * @since 3.50.0 + */ + this.nx = 0; + + /** + * The projected y coordinate of this vertex. + * + * @name Phaser.Geom.Mesh.Vertex#vy + * @type {number} + * @since 3.50.0 + */ + this.ny = 0; + + /** + * The projected z coordinate of this vertex. + * + * @name Phaser.Geom.Mesh.Vertex#vz + * @type {number} + * @since 3.50.0 + */ + this.nz = 0; + /** * UV u coordinate of this vertex. * @@ -107,6 +134,15 @@ var Vertex = new Class({ this.alpha = alpha; }, + setNormals: function (nx, ny, nz) + { + this.nx = nx; + this.ny = ny; + this.nz = nz; + + return this; + }, + /** * Transforms this vertex by the given matrix, storing the results in `vx`, `vy` and `vz`. * diff --git a/src/renderer/webgl/PipelineManager.js b/src/renderer/webgl/PipelineManager.js index 151233e63..5785769d6 100644 --- a/src/renderer/webgl/PipelineManager.js +++ b/src/renderer/webgl/PipelineManager.js @@ -14,6 +14,7 @@ var LightPipeline = require('./pipelines/LightPipeline'); var MultiPipeline = require('./pipelines/MultiPipeline'); var RopePipeline = require('./pipelines/RopePipeline'); var SinglePipeline = require('./pipelines/SinglePipeline'); +var MeshPipeline = require('./pipelines/MeshPipeline'); /** * @classdesc @@ -146,6 +147,7 @@ var PipelineManager = new Class({ this.add(CONST.SINGLE_PIPELINE, new SinglePipeline({ game: game })); this.add(CONST.ROPE_PIPELINE, new RopePipeline({ game: game })); this.add(CONST.LIGHT_PIPELINE, new LightPipeline({ game: game })); + this.add(CONST.MESH_PIPELINE, new MeshPipeline({ game: game })); this.set(this.MULTI_PIPELINE); }, diff --git a/src/renderer/webgl/pipelines/MeshPipeline.js b/src/renderer/webgl/pipelines/MeshPipeline.js new file mode 100644 index 000000000..88faec083 --- /dev/null +++ b/src/renderer/webgl/pipelines/MeshPipeline.js @@ -0,0 +1,288 @@ +/** + * @author Richard Davey + * @copyright 2020 Photon Storm Ltd. + * @license {@link https://opensource.org/licenses/MIT|MIT License} + */ + +var Class = require('../../../utils/Class'); +var GetFastValue = require('../../../utils/object/GetFastValue'); +var ShaderSourceFS = require('../shaders/Mesh-frag.js'); +var ShaderSourceVS = require('../shaders/Mesh-vert.js'); +var WebGLPipeline = require('../WebGLPipeline'); + +/** + * @classdesc + * TODO + * + * @class MeshPipeline + * @extends Phaser.Renderer.WebGL.WebGLPipeline + * @memberof Phaser.Renderer.WebGL.Pipelines + * @constructor + * @since 3.50.0 + * + * @param {Phaser.Types.Renderer.WebGL.WebGLPipelineConfig} config - The configuration options for this pipeline. + */ +var MeshPipeline = new Class({ + + Extends: WebGLPipeline, + + initialize: + + function MeshPipeline (config) + { + var gl = config.game.renderer.gl; + + config.fragShader = GetFastValue(config, 'fragShader', ShaderSourceFS), + config.vertShader = GetFastValue(config, 'vertShader', ShaderSourceVS), + config.vertexCapacity = GetFastValue(config, 'vertexCapacity', 8), + config.vertexSize = GetFastValue(config, 'vertexSize', 32), + config.attributes = GetFastValue(config, 'attributes', [ + { + name: 'aVertexPosition', + size: 3, + type: gl.FLOAT, + normalized: false, + offset: 0, + enabled: false, + location: -1 + }, + { + name: 'aVertexNormal', + size: 3, + type: gl.FLOAT, + normalized: false, + offset: 12, + enabled: false, + location: -1 + }, + { + name: 'aTextureCoord', + size: 2, + type: gl.FLOAT, + normalized: false, + offset: 24, + enabled: false, + location: -1 + } + ]); + config.uniforms = GetFastValue(config, 'uniforms', [ + 'uViewProjectionMatrix', + 'uLightPosition', + 'uLightAmbient', + 'uLightDiffuse', + 'uLightSpecular', + 'uCameraPosition', + 'uFogColor', + 'uFogNear', + 'uFogFar', + 'uModelMatrix', + 'uNormalMatrix', + 'uMaterialAmbient', + 'uMaterialDiffuse', + 'uMaterialSpecular', + 'uMaterialShine', + 'uTexture' + ]); + + WebGLPipeline.call(this, config); + + this.forceZero = true; + + // Cache structure: + + // 0 fog near + // 1 fog far + // 2, 3, 4 model material ambient + // 5, 6, 7 model material diffuse + // 8, 9, 10 model material specular + // 11 model material shine + + this.dirtyCache = [ + -1, + -1, + -1, -1, -1, + -1, -1, -1, + -1, -1, -1, + -1 + ]; + + this.cullMode = 1029; + }, + + /** + * Called every time the pipeline is bound by the renderer. + * Sets the shader program, vertex buffer and other resources. + * Should only be called when changing pipeline. + * + * @method Phaser.Renderer.WebGL.Pipelines.MeshPipeline#bind + * @since 3.50.0 + * + * @param {boolean} [reset=false] - Should the pipeline be fully re-bound after a renderer pipeline clear? + * + * @return {this} This WebGLPipeline instance. + */ + bind: function (reset) + { + if (reset === undefined) { reset = false; } + + WebGLPipeline.prototype.bind.call(this, reset); + + var gl = this.gl; + + gl.enable(gl.DEPTH_TEST); + gl.enable(gl.CULL_FACE); + gl.cullFace(gl.BACK); + + return this; + }, + + /** + * This method is called every time a Game Object asks the Pipeline Manager to use this pipeline. + * + * Unlike the `bind` method, which is only called once per frame, this is called for every object + * that requests it, allowing you to perform per-object GL set-up. + * + * @method Phaser.Renderer.WebGL.Pipelines.MeshPipeline#onBind + * @since 3.50.0 + * + * @param {Phaser.GameObjects.Mesh} mesh - The Mesh that requested this pipeline. + * + * @return {this} This WebGLPipeline instance. + */ + onBind: function (mesh) + { + var camera = mesh.camera; + + if (camera.dirtyView || camera.dirtyProjection) + { + this.setMatrix4fv('uViewProjectionMatrix', false, camera.viewProjectionMatrix.val); + + this.set3f('uCameraPosition', camera.x, camera.y, camera.z); + } + + var light = mesh.light; + + if (light.isDirty()) + { + this.set3f('uLightPosition', light.x, light.y, light.z); + } + + var ambient = light.ambient; + var diffuse = light.diffuse; + var specular = light.specular; + + if (ambient.dirty) + { + this.set3f('uLightAmbient', ambient.r, ambient.g, ambient.b); + } + + if (diffuse.dirty) + { + this.set3f('uLightDiffuse', diffuse.r, diffuse.g, diffuse.b); + } + + if (specular.dirty) + { + this.set3f('uLightSpecular', specular.r, specular.g, specular.b); + } + + var fogColor = mesh.fogColor; + + if (fogColor.dirty) + { + this.set3f('uFogColor', fogColor.r, fogColor.g, fogColor.b); + } + + var cache = this.dirtyCache; + var fogNear = mesh.fogNear; + var fogFar = mesh.fogFar; + + if (cache[0] !== fogNear) + { + this.set1f('uFogNear', fogNear); + + cache[0] = fogNear; + } + + if (cache[1] !== fogFar) + { + this.set1f('uFogFar', fogFar); + + cache[1] = fogFar; + } + + this.set1i('uTexture', 0); + }, + + drawModel: function (mesh, model) + { + var cache = this.dirtyCache; + + this.setMatrix4fv('uModelMatrix', false, model.transformMatrix.val); + this.setMatrix4fv('uNormalMatrix', false, model.normalMatrix.val); + + var ambient = model.ambient; + + if (!ambient.equals(cache[2], cache[3], cache[4])) + { + this.set3f('uMaterialAmbient', ambient.r, ambient.g, ambient.b); + + cache[2] = ambient.r; + cache[3] = ambient.g; + cache[4] = ambient.b; + } + + var diffuse = model.diffuse; + + if (!diffuse.equals(cache[5], cache[6], cache[7])) + { + this.set3f('uMaterialDiffuse', diffuse.r, diffuse.g, diffuse.b); + + cache[5] = diffuse.r; + cache[6] = diffuse.g; + cache[7] = diffuse.b; + } + + var specular = model.specular; + + if (!specular.equals(cache[8], cache[9], cache[10])) + { + this.set3f('uMaterialSpecular', specular.r, specular.g, specular.b); + + cache[8] = specular.r; + cache[9] = specular.g; + cache[10] = specular.b; + } + + var shine = model.shine; + + if (!shine !== cache[11]) + { + this.set1f('uMaterialShine', shine); + + cache[11] = specular.b; + } + + this.renderer.setTextureZero(model.frame.glTexture); + + // All the uniforms are finally bound, so let's buffer our data + var gl = this.gl; + + var cullMode = model.cullMode; + + if (cullMode !== this.cullMode) + { + this.cullMode = cullMode; + + gl.cullFace(cullMode); + } + + // STATIC because the buffer data doesn't change, the uniforms do + gl.bufferData(gl.ARRAY_BUFFER, model.vertexData, gl.STATIC_DRAW); + + gl.drawArrays(this.topology, 0, model.vertexCount); + } + +}); + +module.exports = MeshPipeline; diff --git a/src/renderer/webgl/pipelines/const.js b/src/renderer/webgl/pipelines/const.js index fa8171bf6..ae0a20c56 100644 --- a/src/renderer/webgl/pipelines/const.js +++ b/src/renderer/webgl/pipelines/const.js @@ -54,7 +54,17 @@ var PIPELINE_CONST = { * @const * @since 3.50.0 */ - ROPE_PIPELINE: 'RopePipeline' + ROPE_PIPELINE: 'RopePipeline', + + /** + * The Mesh Pipeline. + * + * @name Phaser.Renderer.WebGL.Pipelines.MESH_PIPELINE + * @type {string} + * @const + * @since 3.50.0 + */ + MESH_PIPELINE: 'MeshPipeline' }; diff --git a/src/renderer/webgl/shaders/Mesh-frag.js b/src/renderer/webgl/shaders/Mesh-frag.js new file mode 100644 index 000000000..35a3b1c83 --- /dev/null +++ b/src/renderer/webgl/shaders/Mesh-frag.js @@ -0,0 +1,54 @@ +module.exports = [ + '#define SHADER_NAME PHASER_MESH_FS', + '', + 'precision mediump float;', + '', + 'uniform vec3 uLightPosition;', + 'uniform vec3 uLightAmbient;', + 'uniform vec3 uLightDiffuse;', + 'uniform vec3 uLightSpecular;', + '', + 'uniform vec3 uFogColor;', + 'uniform float uFogNear;', + 'uniform float uFogFar;', + '', + 'uniform vec3 uMaterialAmbient;', + 'uniform vec3 uMaterialDiffuse;', + 'uniform vec3 uMaterialSpecular;', + 'uniform float uMaterialShine;', + '', + 'uniform vec3 uCameraPosition;', + '', + 'uniform sampler2D uTexture;', + '', + 'varying vec2 vTextureCoord;', + 'varying vec3 vNormal;', + 'varying vec3 vPosition;', + '', + 'void main (void)', + '{', + ' vec4 color = texture2D(uTexture, vTextureCoord);', + '', + ' vec3 ambient = uLightAmbient * uMaterialAmbient;', + '', + ' vec3 norm = normalize(vNormal);', + ' vec3 lightDir = normalize(uLightPosition - vPosition);', + ' float diff = max(dot(norm, lightDir), 0.0);', + ' vec3 diffuse = uLightDiffuse * (diff * uMaterialDiffuse);', + '', + ' vec3 viewDir = normalize(uCameraPosition - vPosition);', + ' vec3 reflectDir = reflect(-lightDir, norm);', + ' float spec = pow(max(dot(viewDir, reflectDir), 0.0), uMaterialShine);', + ' vec3 specular = uLightSpecular * (spec * uMaterialSpecular);', + '', + ' vec3 result = (ambient + diffuse + specular) * color.rgb;', + '', + ' float depth = gl_FragCoord.z / gl_FragCoord.w;', + '', + ' float fogFactor = smoothstep(uFogNear, uFogFar, depth);', + '', + ' gl_FragColor.rgb = mix(result.rgb, uFogColor, fogFactor);', + ' gl_FragColor.a = color.a;', + '}', + '' +].join('\n'); diff --git a/src/renderer/webgl/shaders/Mesh-vert.js b/src/renderer/webgl/shaders/Mesh-vert.js new file mode 100644 index 000000000..eed7de6bf --- /dev/null +++ b/src/renderer/webgl/shaders/Mesh-vert.js @@ -0,0 +1,29 @@ +module.exports = [ + '#define SHADER_NAME PHASER_MESH_VS', + '', + 'precision mediump float;', + '', + 'attribute vec3 aVertexPosition;', + 'attribute vec3 aVertexNormal;', + 'attribute vec2 aTextureCoord;', + '', + 'uniform mat4 uViewProjectionMatrix;', + 'uniform mat4 uModelMatrix;', + 'uniform mat4 uNormalMatrix;', + '', + 'varying vec2 vTextureCoord;', + 'varying vec3 vNormal;', + 'varying vec3 vPosition;', + '', + 'void main ()', + '{', + ' vTextureCoord = aTextureCoord;', + '', + ' vPosition = vec3(uModelMatrix * vec4(aVertexPosition, 1.0));', + '', + ' vNormal = vec3(uNormalMatrix * vec4(aVertexNormal, 1.0));', + '', + ' gl_Position = uViewProjectionMatrix * uModelMatrix * vec4(aVertexPosition, 1.0);', + '}', + '' +].join('\n'); diff --git a/src/renderer/webgl/shaders/src/Mesh.frag b/src/renderer/webgl/shaders/src/Mesh.frag new file mode 100644 index 000000000..823a2766c --- /dev/null +++ b/src/renderer/webgl/shaders/src/Mesh.frag @@ -0,0 +1,51 @@ +#define SHADER_NAME PHASER_MESH_FS + +precision mediump float; + +uniform vec3 uLightPosition; +uniform vec3 uLightAmbient; +uniform vec3 uLightDiffuse; +uniform vec3 uLightSpecular; + +uniform vec3 uFogColor; +uniform float uFogNear; +uniform float uFogFar; + +uniform vec3 uMaterialAmbient; +uniform vec3 uMaterialDiffuse; +uniform vec3 uMaterialSpecular; +uniform float uMaterialShine; + +uniform vec3 uCameraPosition; + +uniform sampler2D uTexture; + +varying vec2 vTextureCoord; +varying vec3 vNormal; +varying vec3 vPosition; + +void main (void) +{ + vec4 color = texture2D(uTexture, vTextureCoord); + + vec3 ambient = uLightAmbient * uMaterialAmbient; + + vec3 norm = normalize(vNormal); + vec3 lightDir = normalize(uLightPosition - vPosition); + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = uLightDiffuse * (diff * uMaterialDiffuse); + + vec3 viewDir = normalize(uCameraPosition - vPosition); + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), uMaterialShine); + vec3 specular = uLightSpecular * (spec * uMaterialSpecular); + + vec3 result = (ambient + diffuse + specular) * color.rgb; + + float depth = gl_FragCoord.z / gl_FragCoord.w; + + float fogFactor = smoothstep(uFogNear, uFogFar, depth); + + gl_FragColor.rgb = mix(result.rgb, uFogColor, fogFactor); + gl_FragColor.a = color.a; +} diff --git a/src/renderer/webgl/shaders/src/Mesh.vert b/src/renderer/webgl/shaders/src/Mesh.vert new file mode 100644 index 000000000..94f611766 --- /dev/null +++ b/src/renderer/webgl/shaders/src/Mesh.vert @@ -0,0 +1,26 @@ +#define SHADER_NAME PHASER_MESH_VS + +precision mediump float; + +attribute vec3 aVertexPosition; +attribute vec3 aVertexNormal; +attribute vec2 aTextureCoord; + +uniform mat4 uViewProjectionMatrix; +uniform mat4 uModelMatrix; +uniform mat4 uNormalMatrix; + +varying vec2 vTextureCoord; +varying vec3 vNormal; +varying vec3 vPosition; + +void main () +{ + vTextureCoord = aTextureCoord; + + vPosition = vec3(uModelMatrix * vec4(aVertexPosition, 1.0)); + + vNormal = vec3(uNormalMatrix * vec4(aVertexNormal, 1.0)); + + gl_Position = uViewProjectionMatrix * uModelMatrix * vec4(aVertexPosition, 1.0); +}