Repository: playcanvas/engine (js)

Issue:
Create public API to copy Material properties to another material
- it's convenient to copy all properties of a material to another material
- we have some private APIs for this, but nothing public

Pull request description:
Refactor material cloning and add new public API for material copying
Currently, the implementation of material cloning is a bit messy and does not make particularly smart use of inheritance. This PR addresses that. 

New API:
* `Material#copy` (with implementations for `BasicMaterial` and `StandardMaterial`.

Summary
* Convert `_cloneInternal` functions to public `copy` functions and have `Material#clone` use `copy`.
* Add unit tests for simple material creation, copying and cloning.
* Fix type of `StandardMaterail#occludeDirect`.

Fixes #3284

I confirm I have read the [contributing guidelines](https://github.com/playcanvas/engine/blob/master/.github/CONTRIBUTING.md) and signed the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform).


Patch:
diff --git a/src/scene/materials/basic-material.js b/src/scene/materials/basic-material.js
index ae7027ddb..32db5b8d3 100644
--- a/src/scene/materials/basic-material.js
+++ b/src/scene/materials/basic-material.js
@@ -51,21 +51,19 @@ class BasicMaterial extends Material {
     }
 
     /**
-     * Duplicates a Basic material. All properties are duplicated except textures where only the
-     * references are copied.
+     * Copy a `BasicMaterial`.
      *
-     * @returns {BasicMaterial} A cloned Basic material.
+     * @param {BasicMaterial} source - The material to copy from.
+     * @returns {BasicMaterial} The destination material.
      */
-    clone() {
-        const clone = new BasicMaterial();
+    copy(source) {
+        super.copy(source);
 
-        Material.prototype._cloneInternal.call(this, clone);
+        this.color.copy(source.color);
+        this.colorMap = source.colorMap;
+        this.vertexColors = source.vertexColors;
 
-        clone.color.copy(this.color);
-        clone.colorMap = this.colorMap;
-        clone.vertexColors = this.vertexColors;
-
-        return clone;
+        return this;
     }
 
     updateUniforms(device, scene) {
diff --git a/src/scene/materials/depth-material.js b/src/scene/materials/depth-material.js
index 7cabd1c00..0ea5fd8b1 100644
--- a/src/scene/materials/depth-material.js
+++ b/src/scene/materials/depth-material.js
@@ -6,20 +6,6 @@ import { Material } from './material.js';
  * @private
  */
 class DepthMaterial extends Material {
-    /**
-     * Duplicates a Depth material.
-     *
-     * @returns {DepthMaterial} A cloned Depth material.
-     * @private
-     */
-    clone() {
-        const clone = new DepthMaterial();
-
-        Material.prototype._cloneInternal.call(this, clone);
-
-        return clone;
-    }
-
     updateShader(device) {
         const options = {
             skin: !!this.meshInstances[0].skinInstance
diff --git a/src/scene/materials/material.js b/src/scene/materials/material.js
index 7f6667d7e..00b8c405d 100644
--- a/src/scene/materials/material.js
+++ b/src/scene/materials/material.js
@@ -287,49 +287,61 @@ class Material {
         return BLEND_NORMAL;
     }
 
-    _cloneInternal(clone) {
-        clone.name = this.name;
-        clone.shader = this.shader;
+    /**
+     * Copy a material.
+     *
+     * @param {Material} source - The material to copy.
+     * @returns {Material} The destination material.
+     */
+    copy(source) {
+        this.name = source.name;
+        this.shader = source.shader;
 
         // Render states
-        clone.alphaTest = this.alphaTest;
-        clone.alphaToCoverage = this.alphaToCoverage;
-
-        clone.blend = this.blend;
-        clone.blendSrc = this.blendSrc;
-        clone.blendDst = this.blendDst;
-        clone.blendEquation = this.blendEquation;
-
-        clone.separateAlphaBlend = this.separateAlphaBlend;
-        clone.blendSrcAlpha = this.blendSrcAlpha;
-        clone.blendDstAlpha = this.blendDstAlpha;
-        clone.blendAlphaEquation = this.blendAlphaEquation;
-
-        clone.cull = this.cull;
-
-        clone.depthTest = this.depthTest;
-        clone.depthWrite = this.depthWrite;
-        clone.depthBias = this.depthBias;
-        clone.slopeDepthBias = this.slopeDepthBias;
-        if (this.stencilFront) clone.stencilFront = this.stencilFront.clone();
-        if (this.stencilBack) {
-            if (this.stencilFront === this.stencilBack) {
-                clone.stencilBack = clone.stencilFront;
+        this.alphaTest = source.alphaTest;
+        this.alphaToCoverage = source.alphaToCoverage;
+
+        this.blend = source.blend;
+        this.blendSrc = source.blendSrc;
+        this.blendDst = source.blendDst;
+        this.blendEquation = source.blendEquation;
+
+        this.separateAlphaBlend = source.separateAlphaBlend;
+        this.blendSrcAlpha = source.blendSrcAlpha;
+        this.blendDstAlpha = source.blendDstAlpha;
+        this.blendAlphaEquation = source.blendAlphaEquation;
+
+        this.cull = source.cull;
+
+        this.depthTest = source.depthTest;
+        this.depthWrite = source.depthWrite;
+        this.depthBias = source.depthBias;
+        this.slopeDepthBias = source.slopeDepthBias;
+        if (source.stencilFront) this.stencilFront = source.stencilFront.clone();
+        if (source.stencilBack) {
+            if (source.stencilFront === source.stencilBack) {
+                this.stencilBack = this.stencilFront;
             } else {
-                clone.stencilBack = this.stencilBack.clone();
+                this.stencilBack = source.stencilBack.clone();
             }
         }
 
-        clone.redWrite = this.redWrite;
-        clone.greenWrite = this.greenWrite;
-        clone.blueWrite = this.blueWrite;
-        clone.alphaWrite = this.alphaWrite;
+        this.redWrite = source.redWrite;
+        this.greenWrite = source.greenWrite;
+        this.blueWrite = source.blueWrite;
+        this.alphaWrite = source.alphaWrite;
+
+        return this;
     }
 
+    /**
+     * Clone a material.
+     *
+     * @returns {Material} A newly cloned material.
+     */
     clone() {
-        const clone = new Material();
-        this._cloneInternal(clone);
-        return clone;
+        const clone = new this.constructor();
+        return clone.copy(this);
     }
 
     _updateMeshInstanceKeys() {
diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js
index 1cef84a55..1eeb90e27 100644
--- a/src/scene/materials/standard-material.js
+++ b/src/scene/materials/standard-material.js
@@ -331,8 +331,8 @@ let _params = new Set();
  * specular.
  *
  * @property {number} occludeSpecularIntensity Controls visibility of specular occlusion.
- * @property {number} occludeDirect Tells if AO should darken directional lighting.
- *
+ * @property {boolean} occludeDirect Tells if AO should darken directional lighting. Defaults to
+ * false.
  * @property {boolean} specularAntialias Enables Toksvig AA for mipmapped normal maps with
  * specular.
  * @property {boolean} conserveEnergy Defines how diffuse and specular components are combined when
@@ -519,28 +519,26 @@ class StandardMaterial extends Material {
     /* eslint-enable jsdoc/check-types */
 
     /**
-     * Duplicates a Standard material. All properties are duplicated except textures where only the
-     * references are copied.
+     * Copy a `StandardMaterial`.
      *
-     * @returns {StandardMaterial} A cloned Standard material.
+     * @param {StandardMaterial} source - The material to copy from.
+     * @returns {StandardMaterial} The destination material.
      */
-    clone() {
-        const clone = new StandardMaterial();
-
-        this._cloneInternal(clone);
+    copy(source) {
+        super.copy(source);
 
         // set properties
         Object.keys(_props).forEach((k) => {
-            clone[k] = this[k];
+            this[k] = source[k];
         });
 
         // clone chunks
-        for (const p in this._chunks) {
-            if (this._chunks.hasOwnProperty(p))
-                clone._chunks[p] = this._chunks[p];
+        for (const p in source._chunks) {
+            if (source._chunks.hasOwnProperty(p))
+                this._chunks[p] = source._chunks[p];
         }
 
-        return clone;
+        return this;
     }
 
     _setParameter(name, value) {


Test patch:
diff --git a/test/scene/materials/basic-material.test.mjs b/test/scene/materials/basic-material.test.mjs
new file mode 100644
index 000000000..e71776f11
--- /dev/null
+++ b/test/scene/materials/basic-material.test.mjs
@@ -0,0 +1,51 @@
+import { BasicMaterial } from '../../../src/scene/materials/basic-material.js';
+import { Color } from '../../../src/math/color.js';
+import { Material } from '../../../src/scene/materials/material.js';
+
+import { expect } from 'chai';
+
+describe('BasicMaterial', function () {
+
+    function checkDefaultMaterial(material) {
+        expect(material).to.be.an.instanceof(BasicMaterial);
+        expect(material).to.be.an.instanceof(Material);
+        expect(material.color).to.be.an.instanceof(Color);
+        expect(material.color.r).to.equal(1);
+        expect(material.color.g).to.equal(1);
+        expect(material.color.b).to.equal(1);
+        expect(material.color.a).to.equal(1);
+        expect(material.colorMap).to.be.null;
+        expect(material.vertexColors).to.equal(false);
+    }
+
+    describe('#constructor()', function () {
+
+        it('should create a new instance', function () {
+            const material = new BasicMaterial();
+            checkDefaultMaterial(material);
+        });
+
+    });
+
+    describe('#clone()', function () {
+
+        it('should clone a material', function () {
+            const material = new BasicMaterial();
+            const clone = material.clone();
+            checkDefaultMaterial(clone);
+        });
+
+    });
+
+    describe('#copy()', function () {
+
+        it('should copy a material', function () {
+            const src = new BasicMaterial();
+            const dst = new BasicMaterial();
+            dst.copy(src);
+            checkDefaultMaterial(dst);
+        });
+
+    });
+
+});
diff --git a/test/scene/materials/depth-material.test.mjs b/test/scene/materials/depth-material.test.mjs
new file mode 100644
index 000000000..1991a439a
--- /dev/null
+++ b/test/scene/materials/depth-material.test.mjs
@@ -0,0 +1,43 @@
+import { DepthMaterial } from '../../../src/scene/materials/depth-material.js';
+import { Material } from '../../../src/scene/materials/material.js';
+
+import { expect } from 'chai';
+
+describe('DepthMaterial', function () {
+
+    function checkDefaultMaterial(material) {
+        expect(material).to.be.an.instanceof(DepthMaterial);
+        expect(material).to.be.an.instanceof(Material);
+    }
+
+    describe('#constructor()', function () {
+
+        it('should create a new instance', function () {
+            const material = new DepthMaterial();
+            checkDefaultMaterial(material);
+        });
+
+    });
+
+    describe('#clone()', function () {
+
+        it('should clone a material', function () {
+            const material = new DepthMaterial();
+            const clone = material.clone();
+            checkDefaultMaterial(clone);
+        });
+
+    });
+
+    describe('#copy()', function () {
+
+        it('should copy a material', function () {
+            const src = new DepthMaterial();
+            const dst = new DepthMaterial();
+            dst.copy(src);
+            checkDefaultMaterial(dst);
+        });
+
+    });
+
+});
diff --git a/test/scene/materials/material.test.mjs b/test/scene/materials/material.test.mjs
new file mode 100644
index 000000000..231c12c10
--- /dev/null
+++ b/test/scene/materials/material.test.mjs
@@ -0,0 +1,59 @@
+import { CULLFACE_BACK } from '../../../src/graphics/constants.js';
+import { BLEND_NONE } from '../../../src/scene/constants.js';
+import { Material } from '../../../src/scene/materials/material.js';
+
+import { expect } from 'chai';
+
+describe('Material', function () {
+
+    function checkDefaultMaterial(material) {
+        expect(material).to.be.an.instanceof(Material);
+        expect(material.alphaTest).to.equal(0);
+        expect(material.alphaToCoverage).to.equal(false);
+        expect(material.alphaWrite).to.equal(true);
+        expect(material.blendType).to.equal(BLEND_NONE);
+        expect(material.blueWrite).to.equal(true);
+        expect(material.cull).to.equal(CULLFACE_BACK);
+        expect(material.depthBias).to.equal(0);
+        expect(material.depthTest).to.equal(true);
+        expect(material.depthWrite).to.equal(true);
+        expect(material.greenWrite).to.equal(true);
+        expect(material.name).to.equal('Untitled');
+        expect(material.redWrite).to.equal(true);
+        expect(material.shader).to.be.null;
+        expect(material.slopeDepthBias).to.equal(0);
+        expect(material.stencilBack).to.be.null;
+        expect(material.stencilFront).to.be.null;
+    }
+
+    describe('#constructor()', function () {
+
+        it('should create a new instance', function () {
+            const material = new Material();
+            checkDefaultMaterial(material);
+        });
+
+    });
+
+    describe('#clone()', function () {
+
+        it('should clone a material', function () {
+            const material = new Material();
+            const clone = material.clone();
+            checkDefaultMaterial(clone);
+        });
+
+    });
+
+    describe('#copy()', function () {
+
+        it('should copy a material', function () {
+            const src = new Material();
+            const dst = new Material();
+            dst.copy(src);
+            checkDefaultMaterial(dst);
+        });
+
+    });
+
+});
diff --git a/test/scene/materials/standard-material.test.mjs b/test/scene/materials/standard-material.test.mjs
new file mode 100644
index 000000000..7bd39306d
--- /dev/null
+++ b/test/scene/materials/standard-material.test.mjs
@@ -0,0 +1,292 @@
+import { CUBEPROJ_NONE, DETAILMODE_MUL, FRESNEL_SCHLICK, SPECOCC_AO, SPECULAR_BLINN } from '../../../src/scene/constants.js';
+import { Color } from '../../../src/math/color.js';
+import { Material } from '../../../src/scene/materials/material.js';
+import { StandardMaterial } from '../../../src/scene/materials/standard-material.js';
+import { Vec2 } from '../../../src/math/vec2.js';
+
+import { expect } from 'chai';
+
+describe('StandardMaterial', function () {
+
+    function checkDefaultMaterial(material) {
+        expect(material).to.be.an.instanceof(StandardMaterial);
+        expect(material).to.be.an.instanceof(Material);
+        expect(material.alphaFade).to.equal(1);
+        expect(material.ambient).to.be.an.instanceof(Color);
+        expect(material.ambient.r).to.equal(0.7);
+        expect(material.ambient.g).to.equal(0.7);
+        expect(material.ambient.b).to.equal(0.7);
+        expect(material.ambientTint).to.equal(false);
+        expect(material.anisotropy).to.equal(0);
+
+        expect(material.aoMap).to.be.null;
+        expect(material.aoMapChannel).to.equal('g');
+        expect(material.aoMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.aoMapOffset.x).to.equal(0);
+        expect(material.aoMapOffset.y).to.equal(0);
+        expect(material.aoMapRotation).to.equal(0);
+        expect(material.aoMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.aoMapTiling.x).to.equal(1);
+        expect(material.aoMapTiling.y).to.equal(1);
+        expect(material.aoMapUv).to.equal(0);
+        expect(material.aoVertexColor).to.equal(false);
+        expect(material.aoVertexColorChannel).to.equal('g');
+
+        expect(material.bumpiness).to.equal(1);
+        expect(material.chunks).to.be.empty;
+
+        expect(material.clearCoat).to.equal(0);
+        expect(material.clearCoatBumpiness).to.equal(1);
+        expect(material.clearCoatGlossMap).to.be.null;
+        expect(material.clearCoatGlossMapChannel).to.equal('g');
+        expect(material.clearCoatGlossMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.clearCoatGlossMapOffset.x).to.equal(0);
+        expect(material.clearCoatGlossMapOffset.y).to.equal(0);
+        expect(material.clearCoatGlossMapRotation).to.equal(0);
+        expect(material.clearCoatGlossMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.clearCoatGlossMapTiling.x).to.equal(1);
+        expect(material.clearCoatGlossMapTiling.y).to.equal(1);
+        expect(material.clearCoatGlossMapUv).to.equal(0);
+        expect(material.clearCoatGlossVertexColor).to.equal(false);
+        expect(material.clearCoatGlossVertexColorChannel).to.equal('g');
+        expect(material.clearCoatGlossiness).to.equal(1);
+        expect(material.clearCoatMap).to.be.null;
+        expect(material.clearCoatMapChannel).to.equal('g');
+        expect(material.clearCoatMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.clearCoatMapOffset.x).to.equal(0);
+        expect(material.clearCoatMapOffset.y).to.equal(0);
+        expect(material.clearCoatMapRotation).to.equal(0);
+        expect(material.clearCoatMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.clearCoatMapTiling.x).to.equal(1);
+        expect(material.clearCoatMapTiling.y).to.equal(1);
+        expect(material.clearCoatMapUv).to.equal(0);
+        expect(material.clearCoatNormalMap).to.be.null;
+        expect(material.clearCoatNormalMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.clearCoatNormalMapOffset.x).to.equal(0);
+        expect(material.clearCoatNormalMapOffset.y).to.equal(0);
+        expect(material.clearCoatNormalMapRotation).to.equal(0);
+        expect(material.clearCoatNormalMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.clearCoatNormalMapTiling.x).to.equal(1);
+        expect(material.clearCoatNormalMapTiling.y).to.equal(1);
+        expect(material.clearCoatNormalMapUv).to.equal(0);
+        expect(material.clearCoatVertexColor).to.equal(false);
+        expect(material.clearCoatVertexColorChannel).to.equal('g');
+
+        expect(material.conserveEnergy).to.equal(true);
+
+        expect(material.cubeMap).to.be.null;
+        expect(material.cubeMapProjection).to.equal(CUBEPROJ_NONE);
+        expect(material.cubeMapProjectionBox).to.be.null;
+
+        expect(material.diffuse).to.be.an.instanceof(Color);
+        expect(material.diffuse.r).to.equal(1);
+        expect(material.diffuse.g).to.equal(1);
+        expect(material.diffuse.b).to.equal(1);
+        expect(material.diffuseDetailMap).to.be.null;
+        expect(material.diffuseDetailMapChannel).to.equal('rgb');
+        expect(material.diffuseDetailMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.diffuseDetailMapOffset.x).to.equal(0);
+        expect(material.diffuseDetailMapOffset.y).to.equal(0);
+        expect(material.diffuseDetailMapRotation).to.equal(0);
+        expect(material.diffuseDetailMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.diffuseDetailMapTiling.x).to.equal(1);
+        expect(material.diffuseDetailMapTiling.y).to.equal(1);
+        expect(material.diffuseDetailMapUv).to.equal(0);
+        expect(material.diffuseDetailMode).to.equal(DETAILMODE_MUL);
+        expect(material.diffuseMap).to.be.null;
+        expect(material.diffuseMapChannel).to.equal('rgb');
+        expect(material.diffuseMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.diffuseMapOffset.x).to.equal(0);
+        expect(material.diffuseMapOffset.y).to.equal(0);
+        expect(material.diffuseMapRotation).to.equal(0);
+        expect(material.diffuseMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.diffuseMapTiling.x).to.equal(1);
+        expect(material.diffuseMapTiling.y).to.equal(1);
+        expect(material.diffuseMapUv).to.equal(0);
+        expect(material.diffuseTint).to.equal(false);
+        expect(material.diffuseVertexColor).to.equal(false);
+        expect(material.diffuseVertexColorChannel).to.equal('rgb');
+
+        expect(material.emissive).to.be.an.instanceof(Color);
+        expect(material.emissive.r).to.equal(0);
+        expect(material.emissive.g).to.equal(0);
+        expect(material.emissive.b).to.equal(0);
+        expect(material.emissiveIntensity).to.equal(1);
+        expect(material.emissiveMap).to.be.null;
+        expect(material.emissiveMapChannel).to.equal('rgb');
+        expect(material.emissiveMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.emissiveMapOffset.x).to.equal(0);
+        expect(material.emissiveMapOffset.y).to.equal(0);
+        expect(material.emissiveMapRotation).to.equal(0);
+        expect(material.emissiveMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.emissiveMapTiling.x).to.equal(1);
+        expect(material.emissiveMapTiling.y).to.equal(1);
+        expect(material.emissiveMapUv).to.equal(0);
+        expect(material.emissiveTint).to.equal(false);
+        expect(material.emissiveVertexColor).to.equal(false);
+        expect(material.emissiveVertexColorChannel).to.equal('rgb');
+
+        expect(material.enableGGXSpecular).to.equal(false);
+        expect(material.fresnelModel).to.equal(FRESNEL_SCHLICK);
+
+        expect(material.glossMap).to.be.null;
+        expect(material.glossMapChannel).to.equal('g');
+        expect(material.glossMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.glossMapOffset.x).to.equal(0);
+        expect(material.glossMapOffset.y).to.equal(0);
+        expect(material.glossMapRotation).to.equal(0);
+        expect(material.glossMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.glossMapTiling.x).to.equal(1);
+        expect(material.glossMapTiling.y).to.equal(1);
+        expect(material.glossMapUv).to.equal(0);
+        expect(material.glossVertexColor).to.equal(false);
+        expect(material.glossVertexColorChannel).to.equal('g');
+
+        expect(material.heightMap).to.be.null;
+        expect(material.heightMapChannel).to.equal('g');
+        expect(material.heightMapFactor).to.equal(1);
+        expect(material.heightMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.heightMapOffset.x).to.equal(0);
+        expect(material.heightMapOffset.y).to.equal(0);
+        expect(material.heightMapRotation).to.equal(0);
+        expect(material.heightMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.heightMapTiling.x).to.equal(1);
+        expect(material.heightMapTiling.y).to.equal(1);
+        expect(material.heightMapUv).to.equal(0);
+
+        expect(material.lightMap).to.be.null;
+        expect(material.lightMapChannel).to.equal('rgb');
+        expect(material.lightMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.lightMapOffset.x).to.equal(0);
+        expect(material.lightMapOffset.y).to.equal(0);
+        expect(material.lightMapRotation).to.equal(0);
+        expect(material.lightMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.lightMapTiling.x).to.equal(1);
+        expect(material.lightMapTiling.y).to.equal(1);
+        expect(material.lightMapUv).to.equal(1);
+        expect(material.lightVertexColor).to.equal(false);
+        expect(material.lightVertexColorChannel).to.equal('rgb');
+
+        expect(material.metalness).to.equal(1);
+        expect(material.metalnessMap).to.be.null;
+        expect(material.metalnessMapChannel).to.equal('g');
+        expect(material.metalnessMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.metalnessMapOffset.x).to.equal(0);
+        expect(material.metalnessMapOffset.y).to.equal(0);
+        expect(material.metalnessMapRotation).to.equal(0);
+        expect(material.metalnessMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.metalnessMapTiling.x).to.equal(1);
+        expect(material.metalnessMapTiling.y).to.equal(1);
+        expect(material.metalnessMapUv).to.equal(0);
+        expect(material.metalnessVertexColor).to.equal(false);
+        expect(material.metalnessVertexColorChannel).to.equal('g');
+
+        expect(material.normalDetailMap).to.be.null;
+        expect(material.normalDetailMapBumpiness).to.equal(1);
+        expect(material.normalDetailMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.normalDetailMapOffset.x).to.equal(0);
+        expect(material.normalDetailMapOffset.y).to.equal(0);
+        expect(material.normalDetailMapRotation).to.equal(0);
+        expect(material.normalDetailMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.normalDetailMapTiling.x).to.equal(1);
+        expect(material.normalDetailMapTiling.y).to.equal(1);
+        expect(material.normalDetailMapUv).to.equal(0);
+        expect(material.normalMap).to.be.null;
+        expect(material.normalMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.normalMapOffset.x).to.equal(0);
+        expect(material.normalMapOffset.y).to.equal(0);
+        expect(material.normalMapRotation).to.equal(0);
+        expect(material.normalMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.normalMapTiling.x).to.equal(1);
+        expect(material.normalMapTiling.y).to.equal(1);
+        expect(material.normalMapUv).to.equal(0);
+
+        expect(material.occludeDirect).to.equal(false);
+        expect(material.occludeSpecular).to.equal(SPECOCC_AO);
+        expect(material.occludeSpecularIntensity).to.equal(1);
+
+        expect(material.onUpdateShader).to.be.undefined;
+
+        expect(material.opacity).to.equal(1);
+        expect(material.opacityFadesSpecular).to.equal(true);
+        expect(material.opacityMap).to.be.null;
+        expect(material.opacityMapChannel).to.equal('a');
+        expect(material.opacityMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.opacityMapOffset.x).to.equal(0);
+        expect(material.opacityMapOffset.y).to.equal(0);
+        expect(material.opacityMapRotation).to.equal(0);
+        expect(material.opacityMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.opacityMapTiling.x).to.equal(1);
+        expect(material.opacityMapTiling.y).to.equal(1);
+        expect(material.opacityMapUv).to.equal(0);
+        expect(material.opacityVertexColor).to.equal(false);
+        expect(material.opacityVertexColorChannel).to.equal('a');
+
+        expect(material.pixelSnap).to.equal(false);
+
+        expect(material.reflectivity).to.equal(1);
+        expect(material.refraction).to.equal(0);
+        expect(material.refractionIndex).to.equal(2 / 3);
+        expect(material.shadingModel).to.equal(SPECULAR_BLINN);
+        expect(material.shininess).to.equal(25);
+
+        expect(material.specular).to.be.instanceof(Color);
+        expect(material.specular.r).to.equal(0);
+        expect(material.specular.g).to.equal(0);
+        expect(material.specular.b).to.equal(0);
+        expect(material.specularAntialias).to.equal(false);
+        expect(material.specularMap).to.be.null;
+        expect(material.specularMapChannel).to.equal('rgb');
+        expect(material.specularMapOffset).to.be.an.instanceof(Vec2);
+        expect(material.specularMapOffset.x).to.equal(0);
+        expect(material.specularMapOffset.y).to.equal(0);
+        expect(material.specularMapRotation).to.equal(0);
+        expect(material.specularMapTiling).to.be.an.instanceof(Vec2);
+        expect(material.specularMapTiling.x).to.equal(1);
+        expect(material.specularMapTiling.y).to.equal(1);
+        expect(material.specularMapUv).to.equal(0);
+        expect(material.specularTint).to.equal(false);
+        expect(material.specularVertexColor).to.equal(false);
+        expect(material.specularVertexColorChannel).to.equal('rgb');
+
+        expect(material.sphereMap).to.be.null;
+        expect(material.twoSidedLighting).to.equal(false);
+
+        expect(material.useFog).to.equal(true);
+        expect(material.useGammaTonemap).to.equal(true);
+        expect(material.useLighting).to.equal(true);
+        expect(material.useMetalness).to.equal(false);
+        expect(material.useSkybox).to.equal(true);
+    }
+
+    describe('#constructor()', function () {
+
+        it('should create a new instance', function () {
+            const material = new StandardMaterial();
+            checkDefaultMaterial(material);
+        });
+
+    });
+
+    describe('#clone()', function () {
+
+        it('should clone a material', function () {
+            const material = new StandardMaterial();
+            const clone = material.clone();
+            checkDefaultMaterial(clone);
+        });
+
+    });
+
+    describe('#copy()', function () {
+
+        it('should copy a material', function () {
+            const src = new StandardMaterial();
+            const dst = new StandardMaterial();
+            dst.copy(src);
+            checkDefaultMaterial(dst);
+        });
+
+    });
+
+});