Przeglądaj źródła

MMDLoader: Implement MMDToonMaterial. (#21922)

* Add prototype of MMDToonMaterial.

* Try to use ShaderMaterial to simulate MeshToonMaterial.

* Done simulate MeshToonMaterial with ShaderMaterial.

* Clean up MMDToonMaterial.

* Add envMap uniforms.

* Enable merge gradient map with phong material but got warning.

* Enable specular and shininess in MMDToonMaterial.

* Done combining Phong/Toon/Matcap materials.

* Pre PR chores.

* fix typos.

* Set color to diffuse uniform.

* Update e2e test screenshots.

* Set .shininess to original and cut half irradiance.

* Remove dotNL from irradiance and update screenshots.

* Fix with PR comments.

* Modify maps file name storing position.

* Clean extra code.

* Update comment and use setter/getters to improve compatibility.

* Simplify defineProterties with defineProterty.

* Improve exposing property pattern.

* Simplify defineProterties with defineProterty.
bill42362 4 lat temu
rodzic
commit
5d1d3064f7

+ 106 - 10
examples/js/loaders/MMDLoader.js

@@ -921,7 +921,7 @@
    * @param {BufferGeometry} geometry - some properties are dependend on geometry
    * @param {function} onProgress
    * @param {function} onError
-   * @return {Array<MeshToonMaterial>}
+   * @return {Array<MMDToonMaterial>}
    */
 
 
@@ -937,23 +937,26 @@
 
 				const material = data.materials[ i ];
 				const params = {
-					userData: {}
+					userData: {
+						MMD: {}
+					}
 				};
 				if ( material.name !== undefined ) params.name = material.name;
 				/*
       	 * THREE.Color
       	 *
-      	 * MMD         THREE.MeshToonMaterial
-      	 * diffuse  -  color
+      	 * MMD         MMDToonMaterial
       	 * ambient  -  emissive * a
       	 *               (a = 1.0 without map texture or 0.2 with map texture)
       	 *
-      	 * THREE.MeshToonMaterial doesn't have ambient. Set it to emissive instead.
+      	 * MMDToonMaterial doesn't have ambient. Set it to emissive instead.
       	 * It'll be too bright if material has map texture so using coef 0.2.
       	 */
 
-				params.color = new THREE.Color().fromArray( material.diffuse );
+				params.diffuse = new THREE.Color().fromArray( material.diffuse );
 				params.opacity = material.diffuse[ 3 ];
+				params.specular = new THREE.Color().fromArray( material.specular );
+				params.shininess = material.shininess;
 				params.emissive = new THREE.Color().fromArray( material.ambient );
 				params.transparent = params.opacity !== 1.0; //
 
@@ -1016,15 +1019,21 @@
 					// map
 					if ( material.textureIndex !== - 1 ) {
 
-						params.map = this._loadTexture( data.textures[ material.textureIndex ], textures );
+						params.map = this._loadTexture( data.textures[ material.textureIndex ], textures ); // Since PMX spec don't have standard to list map files except color map and env map,
+						// we need to save file name for further mapping, like matching normal map file names after model loaded.
+						// ref: https://gist.github.com/felixjones/f8a06bd48f9da9a4539f#texture
+
+						params.userData.MMD.mapFileName = data.textures[ material.textureIndex ];
 
 					} // envMap TODO: support m.envFlag === 3
 
 
 					if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) {
 
-						params.envMap = this._loadTexture( data.textures[ material.envTextureIndex ], textures );
-						params.combine = material.envFlag === 1 ? THREE.MultiplyOperation : THREE.AddOperation;
+						params.matcap = this._loadTexture( data.textures[ material.envTextureIndex ], textures ); // Same as color map above, keep file name in userData for further usage.
+
+						params.userData.MMD.matcapFileName = data.textures[ material.envTextureIndex ];
+						params.matcapCombine = material.envFlag === 1 ? THREE.MultiplyOperation : THREE.AddOperation;
 
 					} // gradientMap
 
@@ -1070,7 +1079,7 @@
 
 				}
 
-				materials.push( new THREE.MeshToonMaterial( params ) );
+				materials.push( new MMDToonMaterial( params ) );
 
 			}
 
@@ -1811,6 +1820,93 @@
 
 	}
 
+	class MMDToonMaterial extends THREE.ShaderMaterial {
+
+		constructor( parameters ) {
+
+			super();
+			this._matcapCombine = THREE.AddOperation;
+			this.emissiveIntensity = 1.0;
+			this.normalMapType = THREE.TangentSpaceNormalMap;
+			this.combine = THREE.MultiplyOperation;
+			this.wireframeLinecap = 'round';
+			this.wireframeLinejoin = 'round';
+			this.flatShading = false;
+			this.lights = true;
+			this.vertexShader = THREE.MMDToonShader.vertexShader;
+			this.fragmentShader = THREE.MMDToonShader.fragmentShader;
+			this.defines = Object.assign( {}, THREE.MMDToonShader.defines );
+			Object.defineProperty( this, 'matcapCombine', {
+				get: function () {
+
+					return this._matcapCombine;
+
+				},
+				set: function ( value ) {
+
+					this._matcapCombine = value;
+
+					switch ( value ) {
+
+						case THREE.MultiplyOperation:
+							this.defines.MATCAP_BLENDING_MULTIPLY = true;
+							delete this.defines.MATCAP_BLENDING_ADD;
+							break;
+
+						default:
+						case THREE.AddOperation:
+							this.defines.MATCAP_BLENDING_ADD = true;
+							delete this.defines.MATCAP_BLENDING_MULTIPLY;
+							break;
+
+					}
+
+				}
+			} );
+			this.uniforms = THREE.UniformsUtils.clone( THREE.MMDToonShader.uniforms ); // merged from MeshToon/Phong/MatcapMaterial
+
+			const exposePropertyNames = [ 'specular', 'shininess', 'opacity', 'diffuse', 'map', 'matcap', 'gradientMap', 'lightMap', 'lightMapIntensity', 'aoMap', 'aoMapIntensity', 'emissive', 'emissiveMap', 'bumpMap', 'bumpScale', 'normalMap', 'normalScale', 'displacemantBias', 'displacemantMap', 'displacemantScale', 'specularMap', 'alphaMap', 'envMap', 'reflectivity', 'refractionRatio' ];
+
+			for ( const propertyName of exposePropertyNames ) {
+
+				Object.defineProperty( this, propertyName, {
+					get: function () {
+
+						return this.uniforms[ propertyName ].value;
+
+					},
+					set: function ( value ) {
+
+						this.uniforms[ propertyName ].value = value;
+
+					}
+				} );
+
+			}
+
+			Object.defineProperty( this, 'color', Object.getOwnPropertyDescriptor( this, 'diffuse' ) );
+			this.setValues( parameters );
+
+		}
+
+		copy( source ) {
+
+			super.copy( source );
+			this.matcapCombine = source.matcapCombine;
+			this.emissiveIntensity = source.emissiveIntensity;
+			this.normalMapType = source.normalMapType;
+			this.combine = source.combine;
+			this.wireframeLinecap = source.wireframeLinecap;
+			this.wireframeLinejoin = source.wireframeLinejoin;
+			this.flatShading = source.flatShading;
+			return this;
+
+		}
+
+	}
+
+	MMDToonMaterial.prototype.isMMDToonMaterial = true;
+
 	THREE.MMDLoader = MMDLoader;
 
 } )();

+ 110 - 0
examples/js/shaders/MMDToonShader.js

@@ -0,0 +1,110 @@
+( function () {
+
+	/**
+ * MMD Toon Shader
+ *
+ * This shader is extended from MeshPhongMaterial, and merged algorithms with
+ * MeshToonMaterial and MeshMetcapMaterial.
+ * Ideas came from https://github.com/mrdoob/three.js/issues/19609
+ *
+ * Combining steps:
+ *  * Declare matcap uniform.
+ *  * Add gradientmap_pars_fragment.
+ *  * Use gradient irradiances instead of dotNL irradiance from MeshPhongMaterial.
+ *    (Replace lights_phong_pars_fragment with lights_mmd_toon_pars_fragment)
+ *  * Add mmd_toon_matcap_fragment.
+ */
+	const lights_mmd_toon_pars_fragment = `
+varying vec3 vViewPosition;
+
+#ifndef FLAT_SHADED
+
+	varying vec3 vNormal;
+
+#endif
+
+
+struct BlinnPhongMaterial {
+
+	vec3 diffuseColor;
+	vec3 specularColor;
+	float specularShininess;
+	float specularStrength;
+
+};
+
+void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+
+	vec3 irradiance = getGradientIrradiance( geometry.normal, directLight.direction ) * directLight.color;
+
+	#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+		irradiance *= PI; // punctual light
+
+	#endif
+
+	reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
+
+	reflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong( directLight, geometry, material.specularColor, material.specularShininess ) * material.specularStrength;
+
+}
+
+void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+
+	reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
+
+}
+
+#define RE_Direct				RE_Direct_BlinnPhong
+#define RE_IndirectDiffuse		RE_IndirectDiffuse_BlinnPhong
+
+#define Material_LightProbeLOD( material )	(0)
+`;
+	const mmd_toon_matcap_fragment = `
+#ifdef USE_MATCAP
+
+	vec3 viewDir = normalize( vViewPosition );
+	vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );
+	vec3 y = cross( viewDir, x );
+	vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; // 0.495 to remove artifacts caused by undersized matcap disks
+	vec4 matcapColor = texture2D( matcap, uv );
+	matcapColor = matcapTexelToLinear( matcapColor );
+
+	#ifdef MATCAP_BLENDING_MULTIPLY
+
+		outgoingLight *= matcapColor.rgb;
+
+	#elif defined( MATCAP_BLENDING_ADD )
+
+		outgoingLight += matcapColor.rgb;
+
+	#endif
+
+#endif
+`;
+	const MMDToonShader = {
+		defines: {
+			TOON: true,
+			MATCAP: true,
+			MATCAP_BLENDING_ADD: true
+		},
+		uniforms: THREE.UniformsUtils.merge( [ THREE.ShaderLib.toon.uniforms, THREE.ShaderLib.phong.uniforms, THREE.ShaderLib.matcap.uniforms ] ),
+		vertexShader: THREE.ShaderLib.phong.vertexShader,
+		fragmentShader: THREE.ShaderLib.phong.fragmentShader.replace( '#include <common>', `
+					#ifdef USE_MATCAP
+						uniform sampler2D matcap;
+					#endif
+
+					#include <common>
+				` ).replace( '#include <envmap_common_pars_fragment>', `
+					#include <gradientmap_pars_fragment>
+					#include <envmap_common_pars_fragment>
+				` ).replace( '#include <lights_phong_pars_fragment>', lights_mmd_toon_pars_fragment ).replace( '#include <envmap_fragment>', `
+					#include <envmap_fragment>
+					${mmd_toon_matcap_fragment}
+				` )
+	};
+
+	THREE.MMDToonShader = MMDToonShader;
+
+} )();

+ 169 - 10
examples/jsm/loaders/MMDLoader.js

@@ -5,6 +5,7 @@ import {
 	BufferGeometry,
 	Color,
 	CustomBlending,
+	TangentSpaceNormalMap,
 	DoubleSide,
 	DstAlphaFactor,
 	Euler,
@@ -14,7 +15,8 @@ import {
 	Interpolant,
 	Loader,
 	LoaderUtils,
-	MeshToonMaterial,
+	UniformsUtils,
+	ShaderMaterial,
 	MultiplyOperation,
 	NearestFilter,
 	NumberKeyframeTrack,
@@ -35,6 +37,7 @@ import {
 	RGB_ETC1_Format,
 	RGB_ETC2_Format
 } from '../../../build/three.module.js';
+import { MMDToonShader } from '../shaders/MMDToonShader.js';
 import { TGALoader } from '../loaders/TGALoader.js';
 import { MMDParser } from '../libs/mmdparser.module.js';
 
@@ -1075,7 +1078,7 @@ class MaterialBuilder {
 	 * @param {BufferGeometry} geometry - some properties are dependend on geometry
 	 * @param {function} onProgress
 	 * @param {function} onError
-	 * @return {Array<MeshToonMaterial>}
+	 * @return {Array<MMDToonMaterial>}
 	 */
 	build( data, geometry /*, onProgress, onError */ ) {
 
@@ -1091,23 +1094,24 @@ class MaterialBuilder {
 
 			const material = data.materials[ i ];
 
-			const params = { userData: {} };
+			const params = { userData: { MMD: {} } };
 
 			if ( material.name !== undefined ) params.name = material.name;
 
 			/*
 				 * Color
 				 *
-				 * MMD         MeshToonMaterial
-				 * diffuse  -  color
+				 * MMD         MMDToonMaterial
 				 * ambient  -  emissive * a
 				 *               (a = 1.0 without map texture or 0.2 with map texture)
 				 *
-				 * MeshToonMaterial doesn't have ambient. Set it to emissive instead.
+				 * MMDToonMaterial doesn't have ambient. Set it to emissive instead.
 				 * It'll be too bright if material has map texture so using coef 0.2.
 				 */
-			params.color = new Color().fromArray( material.diffuse );
+			params.diffuse = new Color().fromArray( material.diffuse );
 			params.opacity = material.diffuse[ 3 ];
+			params.specular = new Color().fromArray( material.specular );
+			params.shininess = material.shininess;
 			params.emissive = new Color().fromArray( material.ambient );
 			params.transparent = params.opacity !== 1.0;
 
@@ -1199,18 +1203,26 @@ class MaterialBuilder {
 
 					params.map = this._loadTexture( data.textures[ material.textureIndex ], textures );
 
+					// Since PMX spec don't have standard to list map files except color map and env map,
+					// we need to save file name for further mapping, like matching normal map file names after model loaded.
+					// ref: https://gist.github.com/felixjones/f8a06bd48f9da9a4539f#texture
+					params.userData.MMD.mapFileName = data.textures[ material.textureIndex ];
+
 				}
 
 				// envMap TODO: support m.envFlag === 3
 
 				if ( material.envTextureIndex !== - 1 && ( material.envFlag === 1 || material.envFlag == 2 ) ) {
 
-					params.envMap = this._loadTexture(
+					params.matcap = this._loadTexture(
 						data.textures[ material.envTextureIndex ],
 						textures
 					);
 
-					params.combine = material.envFlag === 1
+					// Same as color map above, keep file name in userData for further usage.
+					params.userData.MMD.matcapFileName = data.textures[ material.envTextureIndex ];
+
+					params.matcapCombine = material.envFlag === 1
 						? MultiplyOperation
 						: AddOperation;
 
@@ -1263,7 +1275,7 @@ class MaterialBuilder {
 
 			}
 
-			materials.push( new MeshToonMaterial( params ) );
+			materials.push( new MMDToonMaterial( params ) );
 
 		}
 
@@ -2064,4 +2076,151 @@ class CubicBezierInterpolation extends Interpolant {
 
 }
 
+class MMDToonMaterial extends ShaderMaterial {
+
+	constructor( parameters ) {
+
+		super();
+
+		this._matcapCombine = AddOperation;
+		this.emissiveIntensity = 1.0;
+		this.normalMapType = TangentSpaceNormalMap;
+
+		this.combine = MultiplyOperation;
+
+		this.wireframeLinecap = 'round';
+		this.wireframeLinejoin = 'round';
+
+		this.flatShading = false;
+
+		this.lights = true;
+
+		this.vertexShader = MMDToonShader.vertexShader;
+		this.fragmentShader = MMDToonShader.fragmentShader;
+
+		this.defines = Object.assign( {}, MMDToonShader.defines );
+		Object.defineProperty( this, 'matcapCombine', {
+
+			get: function () {
+
+				return this._matcapCombine;
+
+			},
+
+			set: function ( value ) {
+
+				this._matcapCombine = value;
+
+				switch ( value ) {
+
+					case MultiplyOperation:
+						this.defines.MATCAP_BLENDING_MULTIPLY = true;
+						delete this.defines.MATCAP_BLENDING_ADD;
+						break;
+
+					default:
+					case AddOperation:
+						this.defines.MATCAP_BLENDING_ADD = true;
+						delete this.defines.MATCAP_BLENDING_MULTIPLY;
+						break;
+
+				}
+
+			},
+
+		} );
+
+		this.uniforms = UniformsUtils.clone( MMDToonShader.uniforms );
+
+		// merged from MeshToon/Phong/MatcapMaterial
+		const exposePropertyNames = [
+			'specular',
+			'shininess',
+			'opacity',
+			'diffuse',
+
+			'map',
+			'matcap',
+			'gradientMap',
+
+			'lightMap',
+			'lightMapIntensity',
+
+			'aoMap',
+			'aoMapIntensity',
+
+			'emissive',
+			'emissiveMap',
+
+			'bumpMap',
+			'bumpScale',
+
+			'normalMap',
+			'normalScale',
+
+			'displacemantBias',
+			'displacemantMap',
+			'displacemantScale',
+
+			'specularMap',
+
+			'alphaMap',
+
+			'envMap',
+			'reflectivity',
+			'refractionRatio',
+		];
+		for ( const propertyName of exposePropertyNames ) {
+
+			Object.defineProperty( this, propertyName, {
+
+				get: function () {
+
+					return this.uniforms[ propertyName ].value;
+
+				},
+
+				set: function ( value ) {
+
+					this.uniforms[ propertyName ].value = value;
+
+				},
+
+			} );
+
+		}
+
+		Object.defineProperty(
+			this,
+			'color',
+			Object.getOwnPropertyDescriptor( this, 'diffuse' )
+		);
+
+		this.setValues( parameters );
+
+	}
+
+	copy( source ) {
+
+		super.copy( source );
+
+		this.matcapCombine = source.matcapCombine;
+		this.emissiveIntensity = source.emissiveIntensity;
+		this.normalMapType = source.normalMapType;
+
+		this.combine = source.combine;
+
+		this.wireframeLinecap = source.wireframeLinecap;
+		this.wireframeLinejoin = source.wireframeLinejoin;
+
+		this.flatShading = source.flatShading;
+
+		return this;
+
+	}
+
+}
+
+MMDToonMaterial.prototype.isMMDToonMaterial = true;
+
 export { MMDLoader };

+ 137 - 0
examples/jsm/shaders/MMDToonShader.js

@@ -0,0 +1,137 @@
+/**
+ * MMD Toon Shader
+ *
+ * This shader is extended from MeshPhongMaterial, and merged algorithms with
+ * MeshToonMaterial and MeshMetcapMaterial.
+ * Ideas came from https://github.com/mrdoob/three.js/issues/19609
+ *
+ * Combining steps:
+ *  * Declare matcap uniform.
+ *  * Add gradientmap_pars_fragment.
+ *  * Use gradient irradiances instead of dotNL irradiance from MeshPhongMaterial.
+ *    (Replace lights_phong_pars_fragment with lights_mmd_toon_pars_fragment)
+ *  * Add mmd_toon_matcap_fragment.
+ */
+
+import { UniformsUtils, ShaderLib } from '../../../build/three.module.js';
+
+const lights_mmd_toon_pars_fragment = `
+varying vec3 vViewPosition;
+
+#ifndef FLAT_SHADED
+
+	varying vec3 vNormal;
+
+#endif
+
+
+struct BlinnPhongMaterial {
+
+	vec3 diffuseColor;
+	vec3 specularColor;
+	float specularShininess;
+	float specularStrength;
+
+};
+
+void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+
+	vec3 irradiance = getGradientIrradiance( geometry.normal, directLight.direction ) * directLight.color;
+
+	#ifndef PHYSICALLY_CORRECT_LIGHTS
+
+		irradiance *= PI; // punctual light
+
+	#endif
+
+	reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
+
+	reflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong( directLight, geometry, material.specularColor, material.specularShininess ) * material.specularStrength;
+
+}
+
+void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {
+
+	reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );
+
+}
+
+#define RE_Direct				RE_Direct_BlinnPhong
+#define RE_IndirectDiffuse		RE_IndirectDiffuse_BlinnPhong
+
+#define Material_LightProbeLOD( material )	(0)
+`;
+
+const mmd_toon_matcap_fragment = `
+#ifdef USE_MATCAP
+
+	vec3 viewDir = normalize( vViewPosition );
+	vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );
+	vec3 y = cross( viewDir, x );
+	vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; // 0.495 to remove artifacts caused by undersized matcap disks
+	vec4 matcapColor = texture2D( matcap, uv );
+	matcapColor = matcapTexelToLinear( matcapColor );
+
+	#ifdef MATCAP_BLENDING_MULTIPLY
+
+		outgoingLight *= matcapColor.rgb;
+
+	#elif defined( MATCAP_BLENDING_ADD )
+
+		outgoingLight += matcapColor.rgb;
+
+	#endif
+
+#endif
+`;
+
+const MMDToonShader = {
+
+	defines: {
+		TOON: true,
+		MATCAP: true,
+		MATCAP_BLENDING_ADD: true,
+	},
+
+	uniforms: UniformsUtils.merge( [
+		ShaderLib.toon.uniforms,
+		ShaderLib.phong.uniforms,
+		ShaderLib.matcap.uniforms,
+	] ),
+
+	vertexShader: ShaderLib.phong.vertexShader,
+
+	fragmentShader:
+		ShaderLib.phong.fragmentShader
+			.replace(
+				'#include <common>',
+				`
+					#ifdef USE_MATCAP
+						uniform sampler2D matcap;
+					#endif
+
+					#include <common>
+				`
+			)
+			.replace(
+				'#include <envmap_common_pars_fragment>',
+				`
+					#include <gradientmap_pars_fragment>
+					#include <envmap_common_pars_fragment>
+				`
+			)
+			.replace(
+				'#include <lights_phong_pars_fragment>',
+				lights_mmd_toon_pars_fragment
+			)
+			.replace(
+				'#include <envmap_fragment>',
+				`
+					#include <envmap_fragment>
+					${mmd_toon_matcap_fragment}
+				`
+			),
+
+};
+
+export { MMDToonShader };

BIN
examples/screenshots/webgl_loader_mmd.jpg