UNPKG

@animech-public/playcanvas

Version:
582 lines (579 loc) 19.7 kB
import { Color } from '../../core/math/color.js'; import { math } from '../../core/math/math.js'; import { Vec2 } from '../../core/math/vec2.js'; import { ShaderProcessorOptions } from '../../platform/graphics/shader-processor-options.js'; import { CUBEPROJ_BOX, SPECULAR_PHONG, SHADER_DEPTH, SHADER_PICK, SHADER_PREPASS_VELOCITY, SPECOCC_AO, SPECULAR_BLINN, FRESNEL_SCHLICK, CUBEPROJ_NONE, DITHER_NONE, DETAILMODE_MUL } from '../constants.js'; import { ShaderPass } from '../shader-pass.js'; import { EnvLighting } from '../graphics/env-lighting.js'; import { getProgramLibrary } from '../shader-lib/get-program-library.js'; import { _matTex2D, standard } from '../shader-lib/programs/standard.js'; import { Material } from './material.js'; import { StandardMaterialOptionsBuilder } from './standard-material-options-builder.js'; import { standardMaterialTextureParameters, standardMaterialCubemapParameters } from './standard-material-parameters.js'; const _props = {}; const _uniforms = {}; let _params = new Set(); class StandardMaterial extends Material { constructor() { super(); this.userAttributes = new Map(); this._dirtyShader = true; this._assetReferences = {}; this._activeParams = new Set(); this._activeLightingParams = new Set(); this.shaderOptBuilder = new StandardMaterialOptionsBuilder(); this.reset(); } reset() { Object.keys(_props).forEach(name => { this[`_${name}`] = _props[name].value(); }); this._chunks = {}; this._uniformCache = {}; } set shader(shader) {} get shader() { return null; } set chunks(value) { this._dirtyShader = true; this._chunks = value; } get chunks() { this._dirtyShader = true; return this._chunks; } copy(source) { super.copy(source); Object.keys(_props).forEach(k => { this[k] = source[k]; }); for (const p in source._chunks) { if (source._chunks.hasOwnProperty(p)) { this._chunks[p] = source._chunks[p]; } } return this; } setAttribute(name, semantic) { this.userAttributes.set(semantic, name); } _setParameter(name, value) { _params.add(name); this.setParameter(name, value); } _setParameters(parameters) { parameters.forEach(v => { this._setParameter(v.name, v.value); }); } _processParameters(paramsName) { const prevParams = this[paramsName]; prevParams.forEach(param => { if (!_params.has(param)) { delete this.parameters[param]; } }); this[paramsName] = _params; _params = prevParams; _params.clear(); } _updateMap(p) { const mname = `${p}Map`; const map = this[mname]; if (map) { this._setParameter(`texture_${mname}`, map); const tname = `${mname}Transform`; const uniform = this.getUniform(tname); if (uniform) { this._setParameters(uniform); } } } _allocUniform(name, allocFunc) { let uniform = this._uniformCache[name]; if (!uniform) { uniform = allocFunc(); this._uniformCache[name] = uniform; } return uniform; } getUniform(name, device, scene) { return _uniforms[name](this, device, scene); } updateUniforms(device, scene) { const getUniform = name => { return this.getUniform(name, device, scene); }; this._setParameter('material_ambient', getUniform('ambient')); if (!this.diffuseMap || this.diffuseTint) { this._setParameter('material_diffuse', getUniform('diffuse')); } if (this.useMetalness) { if (!this.metalnessMap || this.metalness < 1) { this._setParameter('material_metalness', this.metalness); } if (!this.specularMap || this.specularTint) { this._setParameter('material_specular', getUniform('specular')); } if (!this.specularityFactorMap || this.specularityFactorTint) { this._setParameter('material_specularityFactor', this.specularityFactor); } if (!this.sheenMap || this.sheenTint) { this._setParameter('material_sheen', getUniform('sheen')); } if (!this.sheenGlossMap || this.sheenGlossTint) { this._setParameter('material_sheenGloss', this.sheenGloss); } this._setParameter('material_refractionIndex', this.refractionIndex); } else { if (!this.specularMap || this.specularTint) { this._setParameter('material_specular', getUniform('specular')); } } if (this.enableGGXSpecular) { this._setParameter('material_anisotropy', this.anisotropy); } if (this.clearCoat > 0) { this._setParameter('material_clearCoat', this.clearCoat); this._setParameter('material_clearCoatGloss', this.clearCoatGloss); this._setParameter('material_clearCoatBumpiness', this.clearCoatBumpiness); } this._setParameter('material_gloss', getUniform('gloss')); if (!this.emissiveMap || this.emissiveTint) { this._setParameter('material_emissive', getUniform('emissive')); } if (this.emissiveIntensity !== 1) { this._setParameter('material_emissiveIntensity', this.emissiveIntensity); } if (this.refraction > 0) { this._setParameter('material_refraction', this.refraction); } if (this.dispersion > 0) { this._setParameter('material_dispersion', this.dispersion); } if (this.useDynamicRefraction) { this._setParameter('material_thickness', this.thickness); this._setParameter('material_attenuation', getUniform('attenuation')); this._setParameter('material_invAttenuationDistance', this.attenuationDistance === 0 ? 0 : 1.0 / this.attenuationDistance); } if (this.useIridescence) { this._setParameter('material_iridescence', this.iridescence); this._setParameter('material_iridescenceRefractionIndex', this.iridescenceRefractionIndex); this._setParameter('material_iridescenceThicknessMin', this.iridescenceThicknessMin); this._setParameter('material_iridescenceThicknessMax', this.iridescenceThicknessMax); } this._setParameter('material_opacity', this.opacity); if (this.opacityFadesSpecular === false) { this._setParameter('material_alphaFade', this.alphaFade); } if (this.occludeSpecular) { this._setParameter('material_occludeSpecularIntensity', this.occludeSpecularIntensity); } if (this.cubeMapProjection === CUBEPROJ_BOX) { this._setParameter(getUniform('cubeMapProjectionBox')); } for (const p in _matTex2D) { this._updateMap(p); } if (this.ambientSH) { this._setParameter('ambientSH[0]', this.ambientSH); } if (this.normalMap) { this._setParameter('material_bumpiness', this.bumpiness); } if (this.normalMap && this.normalDetailMap) { this._setParameter('material_normalDetailMapBumpiness', this.normalDetailMapBumpiness); } if (this.heightMap) { this._setParameter('material_heightMapFactor', getUniform('heightMapFactor')); } const isPhong = this.shadingModel === SPECULAR_PHONG; if (this.envAtlas && this.cubeMap && !isPhong) { this._setParameter('texture_envAtlas', this.envAtlas); this._setParameter('texture_cubeMap', this.cubeMap); } else if (this.envAtlas && !isPhong) { this._setParameter('texture_envAtlas', this.envAtlas); } else if (this.cubeMap) { this._setParameter('texture_cubeMap', this.cubeMap); } else if (this.sphereMap) { this._setParameter('texture_sphereMap', this.sphereMap); } this._setParameter('material_reflectivity', this.reflectivity); this._processParameters('_activeParams'); if (this._dirtyShader) { this.clearVariants(); } } updateEnvUniforms(device, scene) { const isPhong = this.shadingModel === SPECULAR_PHONG; const hasLocalEnvOverride = this.envAtlas && !isPhong || this.cubeMap || this.sphereMap; if (!hasLocalEnvOverride && this.useSkybox) { if (scene.envAtlas && scene.skybox && !isPhong) { this._setParameter('texture_envAtlas', scene.envAtlas); this._setParameter('texture_cubeMap', scene.skybox); } else if (scene.envAtlas && !isPhong) { this._setParameter('texture_envAtlas', scene.envAtlas); } else if (scene.skybox) { this._setParameter('texture_cubeMap', scene.skybox); } } this._processParameters('_activeLightingParams'); } getShaderVariant(device, scene, objDefs, unused, pass, sortedLights, viewUniformFormat, viewBindGroupFormat, vertexFormat) { this.updateEnvUniforms(device, scene); const shaderPassInfo = ShaderPass.get(device).getByIndex(pass); const minimalOptions = pass === SHADER_DEPTH || pass === SHADER_PICK || pass === SHADER_PREPASS_VELOCITY || shaderPassInfo.isShadow; let options = minimalOptions ? standard.optionsContextMin : standard.optionsContext; if (minimalOptions) { this.shaderOptBuilder.updateMinRef(options, scene, this, objDefs, pass, sortedLights); } else { this.shaderOptBuilder.updateRef(options, scene, this, objDefs, pass, sortedLights); } if (this.onUpdateShader) { options = this.onUpdateShader(options); } const processingOptions = new ShaderProcessorOptions(viewUniformFormat, viewBindGroupFormat, vertexFormat); const library = getProgramLibrary(device); library.register('standard', standard); const shader = library.getProgram('standard', options, processingOptions, this.userId); this._dirtyShader = false; return shader; } destroy() { for (const asset in this._assetReferences) { this._assetReferences[asset]._unbind(); } this._assetReferences = null; super.destroy(); } } StandardMaterial.TEXTURE_PARAMETERS = standardMaterialTextureParameters; StandardMaterial.CUBEMAP_PARAMETERS = standardMaterialCubemapParameters; const defineUniform = (name, getUniformFunc) => { _uniforms[name] = getUniformFunc; }; const definePropInternal = (name, constructorFunc, setterFunc, getterFunc) => { Object.defineProperty(StandardMaterial.prototype, name, { get: getterFunc || function () { return this[`_${name}`]; }, set: setterFunc }); _props[name] = { value: constructorFunc }; }; const defineValueProp = prop => { const internalName = `_${prop.name}`; const dirtyShaderFunc = prop.dirtyShaderFunc || (() => true); const setterFunc = function setterFunc(value) { const oldValue = this[internalName]; if (oldValue !== value) { this._dirtyShader = this._dirtyShader || dirtyShaderFunc(oldValue, value); this[internalName] = value; } }; definePropInternal(prop.name, () => prop.defaultValue, setterFunc, prop.getterFunc); }; const defineAggProp = prop => { const internalName = `_${prop.name}`; const dirtyShaderFunc = prop.dirtyShaderFunc || (() => true); const setterFunc = function setterFunc(value) { const oldValue = this[internalName]; if (!oldValue.equals(value)) { this._dirtyShader = this._dirtyShader || dirtyShaderFunc(oldValue, value); this[internalName] = oldValue.copy(value); } }; definePropInternal(prop.name, () => prop.defaultValue.clone(), setterFunc, prop.getterFunc); }; const defineProp = prop => { return prop.defaultValue && prop.defaultValue.clone ? defineAggProp(prop) : defineValueProp(prop); }; function _defineTex2D(name, channel = 'rgb', vertexColor = true, uv = 0) { _matTex2D[name] = channel.length || -1; defineProp({ name: `${name}Map`, defaultValue: null, dirtyShaderFunc: (oldValue, newValue) => { return !!oldValue !== !!newValue || oldValue && (oldValue.type !== newValue.type || oldValue.fixCubemapSeams !== newValue.fixCubemapSeams || oldValue.format !== newValue.format); } }); defineProp({ name: `${name}MapTiling`, defaultValue: new Vec2(1, 1) }); defineProp({ name: `${name}MapOffset`, defaultValue: new Vec2(0, 0) }); defineProp({ name: `${name}MapRotation`, defaultValue: 0 }); defineProp({ name: `${name}MapUv`, defaultValue: uv }); if (channel) { defineProp({ name: `${name}MapChannel`, defaultValue: channel }); if (vertexColor) { defineProp({ name: `${name}VertexColor`, defaultValue: false }); defineProp({ name: `${name}VertexColorChannel`, defaultValue: channel }); } } const mapTiling = `${name}MapTiling`; const mapOffset = `${name}MapOffset`; const mapRotation = `${name}MapRotation`; const mapTransform = `${name}MapTransform`; defineUniform(mapTransform, (material, device, scene) => { const tiling = material[mapTiling]; const offset = material[mapOffset]; const rotation = material[mapRotation]; if (tiling.x === 1 && tiling.y === 1 && offset.x === 0 && offset.y === 0 && rotation === 0) { return null; } const uniform = material._allocUniform(mapTransform, () => { return [{ name: `texture_${mapTransform}0`, value: new Float32Array(3) }, { name: `texture_${mapTransform}1`, value: new Float32Array(3) }]; }); const cr = Math.cos(rotation * math.DEG_TO_RAD); const sr = Math.sin(rotation * math.DEG_TO_RAD); const uniform0 = uniform[0].value; uniform0[0] = cr * tiling.x; uniform0[1] = -sr * tiling.y; uniform0[2] = offset.x; const uniform1 = uniform[1].value; uniform1[0] = sr * tiling.x; uniform1[1] = cr * tiling.y; uniform1[2] = 1.0 - tiling.y - offset.y; return uniform; }); } function _defineColor(name, defaultValue) { defineProp({ name: name, defaultValue: defaultValue, getterFunc: function () { this._dirtyShader = true; return this[`_${name}`]; } }); defineUniform(name, (material, device, scene) => { const uniform = material._allocUniform(name, () => new Float32Array(3)); const color = material[name]; const gamma = material.useGammaTonemap && scene.gammaCorrection; if (gamma) { uniform[0] = Math.pow(color.r, 2.2); uniform[1] = Math.pow(color.g, 2.2); uniform[2] = Math.pow(color.b, 2.2); } else { uniform[0] = color.r; uniform[1] = color.g; uniform[2] = color.b; } return uniform; }); } function _defineFloat(name, defaultValue, getUniformFunc) { defineProp({ name: name, defaultValue: defaultValue, dirtyShaderFunc: (oldValue, newValue) => { return (oldValue === 0 || oldValue === 1) !== (newValue === 0 || newValue === 1); } }); defineUniform(name, getUniformFunc); } function _defineObject(name, getUniformFunc) { defineProp({ name: name, defaultValue: null, dirtyShaderFunc: (oldValue, newValue) => { return !!oldValue === !!newValue; } }); defineUniform(name, getUniformFunc); } function _defineFlag(name, defaultValue) { defineProp({ name: name, defaultValue: defaultValue }); } function _defineMaterialProps() { _defineColor('ambient', new Color(0.7, 0.7, 0.7)); _defineColor('diffuse', new Color(1, 1, 1)); _defineColor('specular', new Color(0, 0, 0)); _defineColor('emissive', new Color(0, 0, 0)); _defineColor('sheen', new Color(1, 1, 1)); _defineColor('attenuation', new Color(1, 1, 1)); _defineFloat('emissiveIntensity', 1); _defineFloat('specularityFactor', 1); _defineFloat('sheenGloss', 0.0); _defineFloat('gloss', 0.25, (material, device, scene) => { return material.shadingModel === SPECULAR_PHONG ? Math.pow(2, material.gloss * 11) : material.gloss; }); _defineFloat('heightMapFactor', 1, (material, device, scene) => { return material.heightMapFactor * 0.025; }); _defineFloat('opacity', 1); _defineFloat('alphaFade', 1); _defineFloat('alphaTest', 0); _defineFloat('bumpiness', 1); _defineFloat('normalDetailMapBumpiness', 1); _defineFloat('reflectivity', 1); _defineFloat('occludeSpecularIntensity', 1); _defineFloat('refraction', 0); _defineFloat('refractionIndex', 1.0 / 1.5); _defineFloat('dispersion', 0); _defineFloat('thickness', 0); _defineFloat('attenuationDistance', 0); _defineFloat('metalness', 1); _defineFloat('anisotropy', 0); _defineFloat('clearCoat', 0); _defineFloat('clearCoatGloss', 1); _defineFloat('clearCoatBumpiness', 1); _defineFloat('aoUvSet', 0, null); _defineFloat('iridescence', 0); _defineFloat('iridescenceRefractionIndex', 1.0 / 1.5); _defineFloat('iridescenceThicknessMin', 0); _defineFloat('iridescenceThicknessMax', 0); _defineObject('ambientSH'); _defineObject('cubeMapProjectionBox', (material, device, scene) => { const uniform = material._allocUniform('cubeMapProjectionBox', () => { return [{ name: 'envBoxMin', value: new Float32Array(3) }, { name: 'envBoxMax', value: new Float32Array(3) }]; }); const bboxMin = material.cubeMapProjectionBox.getMin(); const minUniform = uniform[0].value; minUniform[0] = bboxMin.x; minUniform[1] = bboxMin.y; minUniform[2] = bboxMin.z; const bboxMax = material.cubeMapProjectionBox.getMax(); const maxUniform = uniform[1].value; maxUniform[0] = bboxMax.x; maxUniform[1] = bboxMax.y; maxUniform[2] = bboxMax.z; return uniform; }); _defineFlag('ambientTint', false); _defineFlag('diffuseTint', false); _defineFlag('sheenTint', false); _defineFlag('specularTint', false); _defineFlag('specularityFactorTint', false); _defineFlag('emissiveTint', false); _defineFlag('fastTbn', false); _defineFlag('useMetalness', false); _defineFlag('useMetalnessSpecularColor', false); _defineFlag('useSheen', false); _defineFlag('enableGGXSpecular', false); _defineFlag('occludeDirect', false); _defineFlag('normalizeNormalMap', true); _defineFlag('conserveEnergy', true); _defineFlag('opacityFadesSpecular', true); _defineFlag('occludeSpecular', SPECOCC_AO); _defineFlag('shadingModel', SPECULAR_BLINN); _defineFlag('fresnelModel', FRESNEL_SCHLICK); _defineFlag('useDynamicRefraction', false); _defineFlag('cubeMapProjection', CUBEPROJ_NONE); _defineFlag('customFragmentShader', null); _defineFlag('useFog', true); _defineFlag('useLighting', true); _defineFlag('useGammaTonemap', true); _defineFlag('useSkybox', true); _defineFlag('forceUv1', false); _defineFlag('pixelSnap', false); _defineFlag('twoSidedLighting', false); _defineFlag('nineSlicedMode', undefined); _defineFlag('msdfTextAttribute', false); _defineFlag('useIridescence', false); _defineFlag('glossInvert', false); _defineFlag('sheenGlossInvert', false); _defineFlag('clearCoatGlossInvert', false); _defineFlag('opacityDither', DITHER_NONE); _defineFlag('opacityShadowDither', DITHER_NONE); _defineTex2D('diffuse'); _defineTex2D('specular'); _defineTex2D('emissive'); _defineTex2D('thickness', 'g'); _defineTex2D('specularityFactor', 'g'); _defineTex2D('normal', ''); _defineTex2D('metalness', 'g'); _defineTex2D('gloss', 'g'); _defineTex2D('opacity', 'a'); _defineTex2D('refraction', 'g'); _defineTex2D('height', 'g', false); _defineTex2D('ao', 'g'); _defineTex2D('light', 'rgb', true, 1); _defineTex2D('msdf', ''); _defineTex2D('diffuseDetail', 'rgb', false); _defineTex2D('normalDetail', ''); _defineTex2D('aoDetail', 'g', false); _defineTex2D('clearCoat', 'g'); _defineTex2D('clearCoatGloss', 'g'); _defineTex2D('clearCoatNormal', ''); _defineTex2D('sheen', 'rgb'); _defineTex2D('sheenGloss', 'g'); _defineTex2D('iridescence', 'g'); _defineTex2D('iridescenceThickness', 'g'); _defineFlag('diffuseDetailMode', DETAILMODE_MUL); _defineFlag('aoDetailMode', DETAILMODE_MUL); _defineObject('cubeMap'); _defineObject('sphereMap'); _defineObject('envAtlas'); const getterFunc = function getterFunc() { return this._prefilteredCubemaps; }; const setterFunc = function setterFunc(value) { const cubemaps = this._prefilteredCubemaps; value = value || []; let changed = false; let complete = true; for (let i = 0; i < 6; ++i) { const v = value[i] || null; if (cubemaps[i] !== v) { cubemaps[i] = v; changed = true; } complete = complete && !!cubemaps[i]; } if (changed) { if (complete) { this.envAtlas = EnvLighting.generatePrefilteredAtlas(cubemaps, { target: this.envAtlas }); } else { if (this.envAtlas) { this.envAtlas.destroy(); this.envAtlas = null; } } this._dirtyShader = true; } }; const empty = [null, null, null, null, null, null]; definePropInternal('prefilteredCubemaps', () => empty.slice(), setterFunc, getterFunc); } _defineMaterialProps(); export { StandardMaterial };