@animech-public/playcanvas
Version:
PlayCanvas WebGL game engine
480 lines (455 loc) • 21.9 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { SHADER_FORWARD, SHADER_FORWARDHDR, SPRITE_RENDERMODE_SLICED, SPRITE_RENDERMODE_TILED, SPECULAR_PHONG, FRESNEL_SCHLICK, BLEND_NONE, DITHER_NONE, DITHER_BAYER8 } from '../../constants.js';
import { ShaderPass } from '../../shader-pass.js';
import { LitShader } from './lit-shader.js';
import { ChunkBuilder } from '../chunk-builder.js';
import { ChunkUtils } from '../chunk-utils.js';
import { StandardMaterialOptions } from '../../materials/standard-material-options.js';
import { LitOptionsUtils } from './lit-options-utils.js';
import { ShaderGenerator } from './shader-generator.js';
const _matTex2D = [];
const buildPropertiesList = options => {
return Object.keys(options).filter(key => key !== 'litOptions').sort();
};
class ShaderGeneratorStandard extends ShaderGenerator {
constructor(...args) {
super(...args);
// Shared Standard Material option structures
this.optionsContext = new StandardMaterialOptions();
this.optionsContextMin = new StandardMaterialOptions();
}
generateKey(options) {
let props;
if (options === this.optionsContextMin) {
if (!this.propsMin) this.propsMin = buildPropertiesList(options);
props = this.propsMin;
} else if (options === this.optionsContext) {
if (!this.props) this.props = buildPropertiesList(options);
props = this.props;
} else {
props = buildPropertiesList(options);
}
const key = `standard:\n${props.map(prop => prop + options[prop]).join('\n')}${LitOptionsUtils.generateKey(options.litOptions)}`;
return key;
}
// get the value to replace $UV with in Map Shader functions
/**
* Get the code with which to to replace '$UV' in the map shader functions.
*
* @param {string} transformPropName - Name of the transform id in the options block. Usually "basenameTransform".
* @param {string} uVPropName - Name of the UV channel in the options block. Usually "basenameUv".
* @param {object} options - The options passed into createShaderDefinition.
* @returns {string} The code used to replace "$UV" in the shader code.
* @private
*/
_getUvSourceExpression(transformPropName, uVPropName, options) {
const transformId = options[transformPropName];
const uvChannel = options[uVPropName];
const isMainPass = options.litOptions.pass === SHADER_FORWARD || options.litOptions.pass === SHADER_FORWARDHDR;
let expression;
if (isMainPass && options.litOptions.nineSlicedMode === SPRITE_RENDERMODE_SLICED) {
expression = 'nineSlicedUv';
} else if (isMainPass && options.litOptions.nineSlicedMode === SPRITE_RENDERMODE_TILED) {
expression = 'nineSlicedUv';
} else {
if (transformId === 0) {
expression = `vUv${uvChannel}`;
} else {
// note: different capitalization!
expression = `vUV${uvChannel}_${transformId}`;
}
// if heightmap is enabled all maps except the heightmap are offset
if (options.heightMap && transformPropName !== 'heightMapTransform') {
expression += ' + dUvOffset';
}
}
return expression;
}
_addMapDef(name, enabled) {
return enabled ? `#define ${name}\n` : `#undef ${name}\n`;
}
_addMapDefs(float, color, vertex, map, invert) {
return this._addMapDef('MAPFLOAT', float) + this._addMapDef('MAPCOLOR', color) + this._addMapDef('MAPVERTEX', vertex) + this._addMapDef('MAPTEXTURE', map) + this._addMapDef('MAPINVERT', invert);
}
/**
* Add chunk for Map Types (used for all maps except Normal).
*
* @param {string} propName - The base name of the map: diffuse | emissive | opacity | light | height | metalness | specular | gloss | ao.
* @param {string} chunkName - The name of the chunk to use. Usually "basenamePS".
* @param {object} options - The options passed into to createShaderDefinition.
* @param {object} chunks - The set of shader chunks to choose from.
* @param {object} mapping - The mapping between chunk and sampler
* @param {string} encoding - The texture's encoding
* @returns {string} The shader code to support this map.
* @private
*/
_addMap(propName, chunkName, options, chunks, mapping, encoding = null) {
const mapPropName = `${propName}Map`;
const uVPropName = `${mapPropName}Uv`;
const identifierPropName = `${mapPropName}Identifier`;
const transformPropName = `${mapPropName}Transform`;
const channelPropName = `${mapPropName}Channel`;
const vertexColorChannelPropName = `${propName}VertexColorChannel`;
const tintPropName = `${propName}Tint`;
const vertexColorPropName = `${propName}VertexColor`;
const detailModePropName = `${propName}Mode`;
const invertName = `${propName}Invert`;
const tintOption = options[tintPropName];
const vertexColorOption = options[vertexColorPropName];
const textureOption = options[mapPropName];
const textureIdentifier = options[identifierPropName];
const detailModeOption = options[detailModePropName];
let subCode = chunks[chunkName];
if (textureOption) {
const uv = this._getUvSourceExpression(transformPropName, uVPropName, options);
subCode = subCode.replace(/\$UV/g, uv).replace(/\$CH/g, options[channelPropName]);
if (mapping && subCode.search(/\$SAMPLER/g) !== -1) {
let samplerName = `texture_${mapPropName}`;
const alias = mapping[textureIdentifier];
if (alias) {
samplerName = alias;
} else {
mapping[textureIdentifier] = samplerName;
}
subCode = subCode.replace(/\$SAMPLER/g, samplerName);
}
if (encoding) {
if (options[channelPropName] === 'aaa') {
// completely skip decoding if the user has selected the alpha channel (since alpha
// is never decoded).
subCode = subCode.replace(/\$DECODE/g, 'passThrough');
} else {
subCode = subCode.replace(/\$DECODE/g, ChunkUtils.decodeFunc(!options.litOptions.gamma && encoding === 'srgb' ? 'linear' : encoding));
}
// continue to support $texture2DSAMPLE
if (subCode.indexOf('$texture2DSAMPLE')) {
const decodeTable = {
linear: 'texture2D',
srgb: 'texture2DSRGB',
rgbm: 'texture2DRGBM',
rgbe: 'texture2DRGBE'
};
subCode = subCode.replace(/\$texture2DSAMPLE/g, decodeTable[encoding] || 'texture2D');
}
}
}
if (vertexColorOption) {
subCode = subCode.replace(/\$VC/g, options[vertexColorChannelPropName]);
}
if (detailModeOption) {
subCode = subCode.replace(/\$DETAILMODE/g, detailModeOption);
}
const isFloatTint = !!(tintOption & 1);
const isVecTint = !!(tintOption & 2);
const invertOption = !!options[invertName];
subCode = this._addMapDefs(isFloatTint, isVecTint, vertexColorOption, textureOption, invertOption) + subCode;
return subCode.replace(/\$/g, '');
}
_correctChannel(p, chan, _matTex2D) {
if (_matTex2D[p] > 0) {
if (_matTex2D[p] < chan.length) {
return chan.substring(0, _matTex2D[p]);
} else if (_matTex2D[p] > chan.length) {
let str = chan;
const chr = str.charAt(str.length - 1);
const addLen = _matTex2D[p] - str.length;
for (let i = 0; i < addLen; i++) str += chr;
return str;
}
return chan;
}
}
/**
* @param {import('../../../platform/graphics/graphics-device.js').GraphicsDevice} device - The
* graphics device.
* @param {StandardMaterialOptions} options - The create options.
* @returns {object} Returns the created shader definition.
* @ignore
*/
createShaderDefinition(device, options) {
const shaderPassInfo = ShaderPass.get(device).getByIndex(options.litOptions.pass);
const isForwardPass = shaderPassInfo.isForward;
const litShader = new LitShader(device, options.litOptions);
// generate vertex shader
const useUv = [];
const useUnmodifiedUv = [];
const mapTransforms = [];
const maxUvSets = 2;
const textureMapping = {};
for (const p in _matTex2D) {
const mname = `${p}Map`;
if (options[`${p}VertexColor`]) {
const cname = `${p}VertexColorChannel`;
options[cname] = this._correctChannel(p, options[cname], _matTex2D);
}
if (options[mname]) {
const cname = `${mname}Channel`;
const tname = `${mname}Transform`;
const uname = `${mname}Uv`;
options[uname] = Math.min(options[uname], maxUvSets - 1);
options[cname] = this._correctChannel(p, options[cname], _matTex2D);
const uvSet = options[uname];
useUv[uvSet] = true;
useUnmodifiedUv[uvSet] = useUnmodifiedUv[uvSet] || options[mname] && !options[tname];
// create map transforms
if (options[tname]) {
mapTransforms.push({
name: p,
id: options[tname],
uv: options[uname]
});
}
}
}
if (options.forceUv1) {
useUv[1] = true;
useUnmodifiedUv[1] = useUnmodifiedUv[1] !== undefined ? useUnmodifiedUv[1] : true;
}
litShader.generateVertexShader(useUv, useUnmodifiedUv, mapTransforms);
// handle fragment shader
if (options.litOptions.shadingModel === SPECULAR_PHONG) {
options.litOptions.fresnelModel = 0;
options.litOptions.ambientSH = false;
} else {
options.litOptions.fresnelModel = options.litOptions.fresnelModel === 0 ? FRESNEL_SCHLICK : options.litOptions.fresnelModel;
}
const decl = new ChunkBuilder();
const code = new ChunkBuilder();
const func = new ChunkBuilder();
const args = new ChunkBuilder();
let lightingUv = '';
// global texture bias for standard textures
if (options.litOptions.nineSlicedMode === SPRITE_RENDERMODE_TILED) {
decl.append('const float textureBias = -1000.0;');
} else {
decl.append('uniform float textureBias;');
}
if (isForwardPass) {
// parallax
if (options.heightMap) {
// if (!options.normalMap) {
// const transformedHeightMapUv = this._getUvSourceExpression("heightMapTransform", "heightMapUv", options);
// if (!options.hasTangents) tbn = tbn.replace(/\$UV/g, transformedHeightMapUv);
// code += tbn;
// }
decl.append('vec2 dUvOffset;');
code.append(this._addMap('height', 'parallaxPS', options, litShader.chunks, textureMapping));
func.append('getParallax();');
}
// opacity
if (options.litOptions.blendType !== BLEND_NONE || options.litOptions.alphaTest || options.litOptions.alphaToCoverage || options.litOptions.opacityDither !== DITHER_NONE) {
decl.append('float dAlpha;');
code.append(this._addMap('opacity', 'opacityPS', options, litShader.chunks, textureMapping));
func.append('getOpacity();');
args.append('litArgs_opacity = dAlpha;');
if (options.litOptions.alphaTest) {
code.append(litShader.chunks.alphaTestPS);
func.append('alphaTest(dAlpha);');
}
const opacityDither = options.litOptions.opacityDither;
if (opacityDither !== DITHER_NONE) {
if (opacityDither === DITHER_BAYER8) {
decl.append(litShader.chunks.bayerPS);
}
decl.append(`#define DITHER_${opacityDither.toUpperCase()}\n`);
decl.append(litShader.chunks.opacityDitherPS);
func.append('opacityDither(dAlpha, 0.0);');
}
} else {
decl.append('float dAlpha = 1.0;');
}
// normal
if (litShader.needsNormal) {
if (options.normalMap || options.clearCoatNormalMap) {
// TODO: let each normalmap input (normalMap, normalDetailMap, clearCoatNormalMap) independently decide which unpackNormal to use.
code.append(options.packedNormal ? litShader.chunks.normalXYPS : litShader.chunks.normalXYZPS);
if (!options.litOptions.hasTangents) {
// TODO: generalize to support each normalmap input (normalMap, normalDetailMap, clearCoatNormalMap) independently
const baseName = options.normalMap ? 'normalMap' : 'clearCoatNormalMap';
lightingUv = this._getUvSourceExpression(`${baseName}Transform`, `${baseName}Uv`, options);
}
}
decl.append('vec3 dNormalW;');
code.append(this._addMap('normalDetail', 'normalDetailMapPS', options, litShader.chunks, textureMapping));
code.append(this._addMap('normal', 'normalMapPS', options, litShader.chunks, textureMapping));
func.append('getNormal();');
args.append('litArgs_worldNormal = dNormalW;');
}
if (litShader.needsSceneColor) {
decl.append('uniform sampler2D uSceneColorMap;');
}
if (litShader.needsScreenSize) {
decl.append('uniform vec4 uScreenSize;');
}
if (litShader.needsTransforms) {
decl.append('uniform mat4 matrix_viewProjection;');
decl.append('uniform mat4 matrix_model;');
}
// support for diffuse & ao detail modes
if (options.diffuseDetail || options.aoDetail) {
code.append(litShader.chunks.detailModesPS);
}
// albedo
decl.append('vec3 dAlbedo;');
if (options.diffuseDetail) {
code.append(this._addMap('diffuseDetail', 'diffuseDetailMapPS', options, litShader.chunks, textureMapping, options.diffuseDetailEncoding));
}
code.append(this._addMap('diffuse', 'diffusePS', options, litShader.chunks, textureMapping, options.diffuseEncoding));
func.append('getAlbedo();');
args.append('litArgs_albedo = dAlbedo;');
if (options.litOptions.useRefraction) {
decl.append('float dTransmission;');
code.append(this._addMap('refraction', 'transmissionPS', options, litShader.chunks, textureMapping));
func.append('getRefraction();');
args.append('litArgs_transmission = dTransmission;');
decl.append('float dThickness;');
code.append(this._addMap('thickness', 'thicknessPS', options, litShader.chunks, textureMapping));
func.append('getThickness();');
args.append('litArgs_thickness = dThickness;');
if (options.litOptions.dispersion) {
args.append('litArgs_dispersion = material_dispersion;');
}
}
if (options.litOptions.useIridescence) {
decl.append('float dIridescence;');
code.append(this._addMap('iridescence', 'iridescencePS', options, litShader.chunks, textureMapping));
func.append('getIridescence();');
args.append('litArgs_iridescence_intensity = dIridescence;');
decl.append('float dIridescenceThickness;');
code.append(this._addMap('iridescenceThickness', 'iridescenceThicknessPS', options, litShader.chunks, textureMapping));
func.append('getIridescenceThickness();');
args.append('litArgs_iridescence_thickness = dIridescenceThickness;');
}
// specularity & glossiness
if (litShader.lighting && options.litOptions.useSpecular || litShader.reflections) {
decl.append('vec3 dSpecularity;');
decl.append('float dGlossiness;');
if (options.litOptions.useSheen) {
decl.append('vec3 sSpecularity;');
code.append(this._addMap('sheen', 'sheenPS', options, litShader.chunks, textureMapping, options.sheenEncoding));
func.append('getSheen();');
args.append('litArgs_sheen_specularity = sSpecularity;');
decl.append('float sGlossiness;');
code.append(this._addMap('sheenGloss', 'sheenGlossPS', options, litShader.chunks, textureMapping));
func.append('getSheenGlossiness();');
args.append('litArgs_sheen_gloss = sGlossiness;');
}
if (options.litOptions.useMetalness) {
decl.append('float dMetalness;');
code.append(this._addMap('metalness', 'metalnessPS', options, litShader.chunks, textureMapping));
func.append('getMetalness();');
args.append('litArgs_metalness = dMetalness;');
decl.append('float dIor;');
code.append(this._addMap('ior', 'iorPS', options, litShader.chunks, textureMapping));
func.append('getIor();');
args.append('litArgs_ior = dIor;');
}
if (options.litOptions.useSpecularityFactor) {
decl.append('float dSpecularityFactor;');
code.append(this._addMap('specularityFactor', 'specularityFactorPS', options, litShader.chunks, textureMapping));
func.append('getSpecularityFactor();');
args.append('litArgs_specularityFactor = dSpecularityFactor;');
}
if (options.useSpecularColor) {
code.append(this._addMap('specular', 'specularPS', options, litShader.chunks, textureMapping, options.specularEncoding));
} else {
code.append('void getSpecularity() { dSpecularity = vec3(1); }');
}
code.append(this._addMap('gloss', 'glossPS', options, litShader.chunks, textureMapping));
func.append('getGlossiness();');
func.append('getSpecularity();');
args.append('litArgs_specularity = dSpecularity;');
args.append('litArgs_gloss = dGlossiness;');
} else {
decl.append('vec3 dSpecularity = vec3(0.0);');
decl.append('float dGlossiness = 0.0;');
}
// ao
if (options.aoDetail) {
code.append(this._addMap('aoDetail', 'aoDetailMapPS', options, litShader.chunks, textureMapping));
}
if (options.aoMap || options.aoVertexColor) {
decl.append('float dAo;');
code.append(this._addMap('ao', 'aoPS', options, litShader.chunks, textureMapping));
func.append('getAO();');
args.append('litArgs_ao = dAo;');
}
// emission
decl.append('vec3 dEmission;');
code.append(this._addMap('emissive', 'emissivePS', options, litShader.chunks, textureMapping, options.emissiveEncoding));
func.append('getEmission();');
args.append('litArgs_emission = dEmission;');
// clearcoat
if (options.litOptions.useClearCoat) {
decl.append('float ccSpecularity;');
decl.append('float ccGlossiness;');
decl.append('vec3 ccNormalW;');
code.append(this._addMap('clearCoat', 'clearCoatPS', options, litShader.chunks, textureMapping));
code.append(this._addMap('clearCoatGloss', 'clearCoatGlossPS', options, litShader.chunks, textureMapping));
code.append(this._addMap('clearCoatNormal', 'clearCoatNormalPS', options, litShader.chunks, textureMapping));
func.append('getClearCoat();');
func.append('getClearCoatGlossiness();');
func.append('getClearCoatNormal();');
args.append('litArgs_clearcoat_specularity = ccSpecularity;');
args.append('litArgs_clearcoat_gloss = ccGlossiness;');
args.append('litArgs_clearcoat_worldNormal = ccNormalW;');
}
// lightmap
if (options.lightMap || options.lightVertexColor) {
const lightmapDir = options.dirLightMap && options.litOptions.useSpecular;
const lightmapChunkPropName = lightmapDir ? 'lightmapDirPS' : 'lightmapSinglePS';
decl.append('vec3 dLightmap;');
if (lightmapDir) {
decl.append('vec3 dLightmapDir;');
}
code.append(this._addMap('light', lightmapChunkPropName, options, litShader.chunks, textureMapping, options.lightMapEncoding));
func.append('getLightMap();');
args.append('litArgs_lightmap = dLightmap;');
if (lightmapDir) {
args.append('litArgs_lightmapDir = dLightmapDir;');
}
}
// only add the legacy chunk if it's referenced
if (code.code.indexOf('texture2DSRGB') !== -1 || code.code.indexOf('texture2DRGBM') !== -1 || code.code.indexOf('texture2DRGBE') !== -1) {
Debug.deprecated('Shader chunk macro $texture2DSAMPLE(XXX) is deprecated. Please use $DECODE(texture2D(XXX)) instead.');
code.prepend(litShader.chunks.textureSamplePS);
}
} else {
// all other passes require only opacity
const opacityShadowDither = options.litOptions.opacityShadowDither;
if (options.litOptions.alphaTest || opacityShadowDither) {
decl.append('float dAlpha;');
code.append(this._addMap('opacity', 'opacityPS', options, litShader.chunks, textureMapping));
func.append('getOpacity();');
args.append('litArgs_opacity = dAlpha;');
if (options.litOptions.alphaTest) {
code.append(litShader.chunks.alphaTestPS);
func.append('alphaTest(dAlpha);');
}
if (opacityShadowDither !== DITHER_NONE) {
if (opacityShadowDither === DITHER_BAYER8) {
decl.append(litShader.chunks.bayerPS);
}
decl.append(`#define DITHER_${opacityShadowDither.toUpperCase()}\n`);
decl.append(litShader.chunks.opacityDitherPS);
func.append('opacityDither(dAlpha, 0.0);');
}
}
}
decl.append(litShader.chunks.litShaderArgsPS);
code.append(`void evaluateFrontend() { \n${func.code}\n${args.code}\n }\n`);
func.code = 'evaluateFrontend();';
for (const texture in textureMapping) {
decl.append(`uniform sampler2D ${textureMapping[texture]};`);
}
// decl.append('//-------- frontend decl begin', decl.code, '//-------- frontend decl end');
// code.append('//-------- frontend code begin', code.code, '//-------- frontend code end');
// func.append('//-------- frontend func begin\n${func}//-------- frontend func end\n`;
// format func
func.code = `\n${func.code.split('\n').map(l => ` ${l}`).join('\n')}\n\n`;
litShader.generateFragmentShader(decl.code, code.code, func.code, lightingUv);
return litShader.getDefinition();
}
}
const standard = new ShaderGeneratorStandard();
export { _matTex2D, standard };