| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Clearcoat Tint Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, ClearcoatTintPlugin, IObject3D, PhysicalMaterial, ThreeViewer} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| }) | |||||
| const clearcoatTint = viewer.addPluginSync(ClearcoatTintPlugin) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', { | |||||
| setBackground: true, | |||||
| }) | |||||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const model = result?.getObjectByName('node_damagedHelmet_-6514') | |||||
| const materials = (model?.materials || []) as PhysicalMaterial[] | |||||
| for (const material of materials) { | |||||
| material.clearcoat = 1 | |||||
| // add initial properties | |||||
| ClearcoatTintPlugin.AddClearcoatTint(material, { | |||||
| tintColor: '#ff0000', | |||||
| thickness: 1, | |||||
| }) | |||||
| // set properties like this or from the UI | |||||
| // material.userData._clearcoatTint!.tintColor = '#ff0000' | |||||
| // Add extra clearcoat tint ui mapped to this material. | |||||
| // This is also added inside the material ui by default by the material extension automatically. | |||||
| const config = material.uiConfig | |||||
| if (!config) continue | |||||
| ui.appendChild(clearcoatTint.materialExtension.getUiConfig?.(material), {expanded: true}) | |||||
| ui.appendChild(config) | |||||
| } | |||||
| } | |||||
| init().then(_testFinish) |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Custom Bump Map Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, CustomBumpMapPlugin, ITexture, Mesh, PhysicalMaterial, PlaneGeometry, ThreeViewer} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| }) | |||||
| const customBump = viewer.addPluginSync(CustomBumpMapPlugin) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| const model = new Mesh(new PlaneGeometry(4, 2), new PhysicalMaterial()) | |||||
| const material = model.material | |||||
| viewer.scene.addObject(model) | |||||
| const bumpMap1 = await viewer.load<ITexture>('https://threejs.org/examples/textures/brick_bump.jpg') | |||||
| const bumpMap2 = await viewer.load<ITexture>('https://threejs.org/examples/textures/planets/earth_specular_2048.jpg') | |||||
| customBump.enableCustomBump(material, bumpMap2, -0.2) | |||||
| material.bumpMap = bumpMap1 || null | |||||
| material.bumpScale = -0.01 | |||||
| material.setDirty() | |||||
| // set properties like this or from the UI | |||||
| // material.userData._customBumpMat = texture | |||||
| // material.setDirty() | |||||
| // to disable | |||||
| // material.userData._hasCustomBump = false | |||||
| // material.setDirty() | |||||
| ui.setupPluginUi(CustomBumpMapPlugin) | |||||
| const config = material.uiConfig! | |||||
| ui.appendChild(customBump.materialExtension.getUiConfig?.(material), {expanded: true}) | |||||
| ui.appendChild(config) | |||||
| } | |||||
| init().then(_testFinish) |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Fragment Clipping Extension Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import { | |||||
| _testFinish, | |||||
| FragmentClippingExtensionPlugin, | |||||
| IObject3D, | |||||
| PhysicalMaterial, | |||||
| ThreeViewer, | |||||
| Vector4, | |||||
| } from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| }) | |||||
| const fragmentClipping = viewer.addPluginSync(FragmentClippingExtensionPlugin) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', { | |||||
| setBackground: true, | |||||
| }) | |||||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const model = result?.getObjectByName('node_damagedHelmet_-6514') | |||||
| const materials = (model?.materials || []) as PhysicalMaterial[] | |||||
| for (const material of materials) { | |||||
| FragmentClippingExtensionPlugin.AddFragmentClipping(material, { | |||||
| clipPosition: new Vector4(0.5, 0.5, 0, 0), | |||||
| clipParams: new Vector4(0.1, 0.05, 0, 1), | |||||
| }) | |||||
| // set properties like this or from the UI | |||||
| // material.userData._fragmentClipping!.clipPosition.set(0, 0, 0, 0) | |||||
| // material.setDirty() | |||||
| // Add extra fragment clipping extension ui mapped to this material. | |||||
| // This is also added inside the material ui by default by the material extension automatically. | |||||
| const config = material.uiConfig | |||||
| if (!config) continue | |||||
| ui.appendChild(fragmentClipping.materialExtension.getUiConfig?.(material), {expanded: true}) | |||||
| ui.appendChild(config) | |||||
| } | |||||
| } | |||||
| init().then(_testFinish) |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>SparkleBump(NoiseBump) Material Plugin</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, IObject3D, NoiseBumpMaterialPlugin, PhysicalMaterial, ThreeViewer} from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| }) | |||||
| const noiseBump = viewer.addPluginSync(NoiseBumpMaterialPlugin) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr', { | |||||
| setBackground: true, | |||||
| }) | |||||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const model = result?.getObjectByName('node_damagedHelmet_-6514') | |||||
| const materials = (model?.materials || []) as PhysicalMaterial[] | |||||
| for (const material of materials) { | |||||
| NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(material, { | |||||
| flakeScale: 300, | |||||
| }) | |||||
| // set properties like this or from the UI | |||||
| // material.userData._noiseBumpMat!.bumpNoiseParams = [1, 1] | |||||
| // material.setDirty() | |||||
| // Add extra noise bump extension ui mapped to this material. | |||||
| // This is also added inside the material ui by default by the material extension automatically. | |||||
| const config = material.uiConfig | |||||
| if (!config) continue | |||||
| ui.appendChild(noiseBump.materialExtension.getUiConfig?.(material), {expanded: true}) | |||||
| ui.appendChild(config) | |||||
| } | |||||
| } | |||||
| init().then(_testFinish) |
| export {PopmotionPlugin} from './animation/PopmotionPlugin' | export {PopmotionPlugin} from './animation/PopmotionPlugin' | ||||
| export {CameraViewPlugin, type CameraViewPluginOptions} from './animation/CameraViewPlugin' | export {CameraViewPlugin, type CameraViewPluginOptions} from './animation/CameraViewPlugin' | ||||
| // material | |||||
| export {ClearcoatTintPlugin} from './material/ClearcoatTintPlugin' | |||||
| export {NoiseBumpMaterialPlugin} from './material/NoiseBumpMaterialPlugin' | |||||
| export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin' | |||||
| export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin' | |||||
| // extras | // extras | ||||
| export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' | export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' |
| import {Color} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' | |||||
| import {glsl, serialize} from 'ts-browser-helpers' | |||||
| import {IMaterialUserData, PhysicalMaterial} from '../../core' | |||||
| import {MaterialExtension, updateMaterialDefines} from '../../materials' | |||||
| import {shaderReplaceString, ThreeSerialization} from '../../utils' | |||||
| import {GLTFLoader2, GLTFWriter2} from '../../assetmanager' | |||||
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| /** | |||||
| * Clearcoat Tint Plugin | |||||
| * Adds a material extension to PhysicalMaterial which adds tint and thickness to the built-in clearcoat properties. | |||||
| * It also adds a UI to the material to edit the settings. | |||||
| * It uses WEBGI_materials_clearcoat_tint glTF extension to save the settings in glTF files. | |||||
| * @category Plugins | |||||
| */ | |||||
| @uiFolderContainer('ClearcoatTint Materials') | |||||
| export class ClearcoatTintPlugin extends AViewerPluginSync<''> { | |||||
| static readonly PluginType = 'ClearcoatTintPlugin' | |||||
| @uiToggle('Enabled', (that: ClearcoatTintPlugin)=>({onChange: that.setDirty})) | |||||
| @serialize() enabled = true | |||||
| // private _defines: any = { | |||||
| // // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| // CLEARCOAT_TINT_DEBUG: false, | |||||
| // } | |||||
| private _uniforms: any = { | |||||
| ccTintColor: {value: new Color()}, | |||||
| ccThickness: {value: 0.}, | |||||
| ccIor: {value: 0.}, | |||||
| } | |||||
| static AddClearcoatTint(material: PhysicalMaterial, params?: IMaterialUserData['_clearcoatTint']): IMaterialUserData['_clearcoatTint']|null { | |||||
| const ud = material?.userData | |||||
| if (!ud) return null | |||||
| if (!ud._clearcoatTint) ud._clearcoatTint = {} | |||||
| const tf = ud._clearcoatTint! | |||||
| tf.enableTint = true | |||||
| if (tf.tintColor === undefined) tf.tintColor = '#ffffff' | |||||
| if (tf.thickness === undefined) tf.thickness = 0.1 | |||||
| if (tf.ior === undefined) tf.ior = 1.5 | |||||
| Object.assign(tf, params) | |||||
| if (material.setDirty) material.setDirty() | |||||
| return tf | |||||
| } | |||||
| // private _multiplyPass?: MultiplyPass | |||||
| readonly materialExtension: MaterialExtension = { | |||||
| parsFragmentSnippet: (_, material: PhysicalMaterial)=>{ | |||||
| if (!this.enabled || !material?.userData._clearcoatTint?.enableTint || !(material.clearcoat > 0)) return '' | |||||
| return glsl` | |||||
| uniform vec3 ccTintColor; | |||||
| uniform float ccThickness; | |||||
| uniform float ccIor; | |||||
| vec3 clearcoatTint(const in float dotNV, const in float dotNL, const in float clearcoat) { | |||||
| vec3 tint = ( ccThickness > 0. ? 1. - ccTintColor : ccTintColor); // Set thickness < 0 for glow. | |||||
| tint = exp(tint * -(ccThickness * ((dotNL + dotNV) / max(dotNL * dotNV, 1e-3)))); // beer's law | |||||
| return mix(vec3(1.0), tint, clearcoat); | |||||
| } | |||||
| ` | |||||
| }, | |||||
| shaderExtender: (shader, material: PhysicalMaterial) => { | |||||
| if (!this.enabled || !material?.userData._clearcoatTint?.enableTint || !(material.clearcoat > 0)) return | |||||
| // Note: clearcoat only considers specular, not diffuse | |||||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, | |||||
| 'float dotNVcc = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) );', | |||||
| 'float dotNVcc = saturate( dot( geometry.clearcoatNormal, -refract(geometry.viewDir, geometry.clearcoatNormal, 1./ccIor) ) );') | |||||
| // todo: we are considering all light is coming from env map, but we should consider light coming from light sources by seperating light and env map attenuation | |||||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, | |||||
| 'outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat;', | |||||
| 'outgoingLight *= clearcoatTint(dotNVcc, dotNVcc, material.clearcoat);\n', {prepend: true}) | |||||
| ;(shader as any).defines.USE_UV = '' | |||||
| }, | |||||
| onObjectRender: (_, material) => { | |||||
| const tfUd = material.userData._clearcoatTint | |||||
| if (!tfUd?.enableTint) return | |||||
| this._uniforms.ccTintColor.value.set(tfUd.tintColor) // could be number or string also, apart from Color | |||||
| this._uniforms.ccThickness.value = tfUd.thickness | |||||
| this._uniforms.ccIor.value = tfUd.ior | |||||
| updateMaterialDefines({ | |||||
| // ...this._defines, | |||||
| ['CLEARCOAT_TINT_ENABLED']: +this.enabled, | |||||
| }, material) | |||||
| }, | |||||
| extraUniforms: { | |||||
| ...this._uniforms, | |||||
| }, | |||||
| computeCacheKey: (material1: PhysicalMaterial) => { | |||||
| return (this.enabled ? '1' : '0') + (material1.userData._clearcoatTint?.enableTint ? '1' : '0') + (material1.clearcoat > 0 ? '1' : '0') | |||||
| }, | |||||
| isCompatible: (material1: PhysicalMaterial) => { | |||||
| return material1.isPhysicalMaterial | |||||
| }, | |||||
| getUiConfig: (material: PhysicalMaterial) => { | |||||
| const viewer = this._viewer! | |||||
| if (material.userData._clearcoatTint === undefined) material.userData._clearcoatTint = {} | |||||
| const state = material.userData._clearcoatTint | |||||
| const config: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Clearcoat Tint', | |||||
| onChange: (ev)=>{ | |||||
| if (!ev.config) return | |||||
| this.setDirty() | |||||
| }, | |||||
| children: [ | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Enabled', | |||||
| get value() { | |||||
| return state.enableTint || false | |||||
| }, | |||||
| set value(v) { | |||||
| if (v === state.enableTint) return | |||||
| if (v) { | |||||
| if (!ClearcoatTintPlugin.AddClearcoatTint(material)) | |||||
| viewer.dialog.alert('Cannot add clearcoat tint.') | |||||
| } else { | |||||
| state.enableTint = false | |||||
| if (material.setDirty) material.setDirty() | |||||
| } | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'color', | |||||
| label: 'Tint color', | |||||
| hidden: () => !state.enableTint, | |||||
| property: [state, 'tintColor'], | |||||
| }, | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Thickness', | |||||
| hidden: () => !state.enableTint, | |||||
| property: [state, 'thickness'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0.8, 2.5], | |||||
| label: 'IOR', | |||||
| hidden: () => !state.enableTint, | |||||
| property: [state, 'ior'], | |||||
| }, | |||||
| ], | |||||
| } | |||||
| return config | |||||
| }, | |||||
| } | |||||
| setDirty = (): void => { | |||||
| this.materialExtension.setDirty?.() | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| private _loaderCreate({loader}: {loader: GLTFLoader2}) { | |||||
| if (!loader.isGLTFLoader2) return | |||||
| loader.register((p) => new GLTFMaterialsClearcoatTintExtensionImport(p)) | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this._loaderCreate = this._loaderCreate.bind(this) | |||||
| } | |||||
| onAdded(v: ThreeViewer) { | |||||
| super.onAdded(v) | |||||
| // v.addEventListener('preRender', this._preRender) | |||||
| v.assetManager.materials.registerMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any) | |||||
| v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsClearcoatTintExtensionExport) | |||||
| } | |||||
| onRemove(v: ThreeViewer) { | |||||
| v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any) | |||||
| const exporter = v.assetManager.exporter.getExporter('gltf', 'glb') | |||||
| if (exporter) { | |||||
| const index = exporter.extensions?.indexOf(glTFMaterialsClearcoatTintExtensionExport) | |||||
| if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1) | |||||
| } | |||||
| return super.onRemove(v) | |||||
| } | |||||
| public static readonly CLEARCOAT_TINT_GLTF_EXTENSION = 'WEBGI_materials_clearcoat_tint' | |||||
| } | |||||
| declare module '../../core/IMaterial' { | |||||
| interface IMaterialUserData { | |||||
| _clearcoatTint?: { | |||||
| enableTint?: boolean | |||||
| tintColor?: Color|number|string | |||||
| thickness?: number | |||||
| ior?: number | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * ClearcoatTint Materials Extension | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_clearcoat_tint.html | |||||
| */ | |||||
| class GLTFMaterialsClearcoatTintExtensionImport implements GLTFLoaderPlugin { | |||||
| public name: string | |||||
| public parser: GLTFParser | |||||
| constructor(parser: GLTFParser) { | |||||
| this.parser = parser | |||||
| this.name = ClearcoatTintPlugin.CLEARCOAT_TINT_GLTF_EXTENSION | |||||
| } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[materialIndex] | |||||
| if (!materialDef.extensions || !materialDef.extensions[this.name]) return | |||||
| const extension = materialDef.extensions[this.name] | |||||
| if (!materialParams.userData) materialParams.userData = {} | |||||
| ClearcoatTintPlugin.AddClearcoatTint(materialParams) | |||||
| ThreeSerialization.Deserialize(extension, materialParams.userData._clearcoatTint) | |||||
| } | |||||
| } | |||||
| const glTFMaterialsClearcoatTintExtensionExport = (w: GLTFWriter2)=> ({ | |||||
| writeMaterial: (material: any, materialDef: any) => { | |||||
| if (!material.isMeshStandardMaterial || !material.userData._clearcoatTint?.enableTint) return | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| const extensionDef: any = ThreeSerialization.Serialize(material.userData._clearcoatTint) | |||||
| materialDef.extensions[ ClearcoatTintPlugin.CLEARCOAT_TINT_GLTF_EXTENSION ] = extensionDef | |||||
| w.extensionsUsed[ ClearcoatTintPlugin.CLEARCOAT_TINT_GLTF_EXTENSION ] = true | |||||
| }, | |||||
| }) |
| import {Matrix3, SRGBColorSpace, Texture} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' | |||||
| import {serialize} from 'ts-browser-helpers' | |||||
| import {IMaterial, IObject3D, ITexture, PhysicalMaterial} from '../../core' | |||||
| import {MaterialExtension, updateMaterialDefines} from '../../materials' | |||||
| import {shaderReplaceString} from '../../utils' | |||||
| import {GLTFLoader2, GLTFWriter2} from '../../assetmanager' | |||||
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import CustomBumpMapPluginShader from './shaders/CustomBumpMapPlugin.glsl' | |||||
| import {matDefine} from '../../three' | |||||
| import {makeSamplerUi} from '../../ui/image-ui' | |||||
| /** | |||||
| * Custom Bump Map Plugin | |||||
| * Adds a material extension to PhysicalMaterial to support custom bump maps. | |||||
| * A Custom bump map is similar to the built-in bump map, but allows using an extra bump map and scale to give a combined effect. | |||||
| * This plugin also has support for bicubic filtering of the custom bump map and is enabled by default. | |||||
| * It also adds a UI to the material to edit the settings. | |||||
| * It uses WEBGI_materials_custom_bump_map glTF extension to save the settings in glTF files. | |||||
| * @category Plugins | |||||
| */ | |||||
| @uiFolderContainer('CustomBumpMap Materials') | |||||
| export class CustomBumpMapPlugin extends AViewerPluginSync<''> { | |||||
| static readonly PluginType = 'CustomBumpMapPlugin' | |||||
| @uiToggle('Enabled', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty})) | |||||
| @serialize() enabled = true | |||||
| @uiToggle('Bicubic', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty})) | |||||
| @matDefine('CUSTOM_BUMP_MAP_BICUBIC', undefined, true, CustomBumpMapPlugin.prototype.setDirty) | |||||
| @serialize() bicubicFiltering = true | |||||
| private _defines: any = { | |||||
| ['CUSTOM_BUMP_MAP_DEBUG']: false, | |||||
| ['CUSTOM_BUMP_MAP_BICUBIC']: true, | |||||
| } | |||||
| private _uniforms: any = { | |||||
| customBumpUvTransform: {value: new Matrix3()}, | |||||
| customBumpScale: {value: 0.001}, | |||||
| customBumpMap: {value: null}, | |||||
| } | |||||
| public enableCustomBump(material: IMaterial, map?: ITexture, scale?: number): boolean { | |||||
| const ud = material?.userData | |||||
| if (!ud) return false | |||||
| if (ud._hasCustomBump === undefined) { | |||||
| const meshes = material.appliedMeshes | |||||
| let possible = true | |||||
| if (meshes) for (const {geometry} of meshes) { | |||||
| if (geometry && (!geometry.attributes.position || !geometry.attributes.normal || !geometry.attributes.uv)) { | |||||
| possible = false | |||||
| } | |||||
| // if (possible && !geometry.attributes.tangent) { | |||||
| // geometry.computeTangents() | |||||
| // } | |||||
| } | |||||
| if (!possible) { | |||||
| return false | |||||
| } | |||||
| } | |||||
| ud._hasCustomBump = true | |||||
| ud._customBumpScale = scale ?? ud._customBumpScale ?? 0.001 | |||||
| ud._customBumpMap = map ?? ud._customBumpMap ?? null | |||||
| if (material.setDirty) material.setDirty() | |||||
| return true | |||||
| } | |||||
| readonly materialExtension: MaterialExtension = { | |||||
| parsFragmentSnippet: (_, material: PhysicalMaterial)=>{ | |||||
| if (!this.enabled || !material?.userData._hasCustomBump) return '' | |||||
| return CustomBumpMapPluginShader | |||||
| }, | |||||
| shaderExtender: (shader, material: PhysicalMaterial) => { | |||||
| if (!this.enabled || !material?.userData._hasCustomBump) return | |||||
| const customBumpMap = material.userData._customBumpMap | |||||
| if (!customBumpMap) return | |||||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker beforeAccumulation', | |||||
| ` | |||||
| #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 | |||||
| normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd_cb(), faceDirection ); | |||||
| #endif | |||||
| `, {prepend: true} | |||||
| ) | |||||
| shader.vertexShader = shaderReplaceString(shader.vertexShader, '#include <uv_pars_vertex>', | |||||
| ` | |||||
| #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 | |||||
| varying vec2 vCustomBumpUv; | |||||
| uniform mat3 customBumpUvTransform; | |||||
| #endif | |||||
| `, {prepend: true}, | |||||
| ) | |||||
| shader.vertexShader = shaderReplaceString(shader.vertexShader, '#include <uv_vertex>', | |||||
| ` | |||||
| #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 | |||||
| vCustomBumpUv = ( customBumpUvTransform * vec3( uv, 1 ) ).xy; | |||||
| #endif | |||||
| `, {prepend: true}, | |||||
| ) | |||||
| ;(shader as any).defines.USE_UV = '' | |||||
| }, | |||||
| onObjectRender: (object: IObject3D, material) => { | |||||
| const userData = material.userData | |||||
| if (!userData?._hasCustomBump) return | |||||
| if (!object.isMesh || !object.geometry) return | |||||
| const tex = userData._customBumpMap?.isTexture ? userData._customBumpMap : null | |||||
| this._uniforms.customBumpMap.value = tex | |||||
| this._uniforms.customBumpScale.value = tex ? userData._customBumpScale ?? 0 : 0 | |||||
| if (tex) { | |||||
| tex.updateMatrix() | |||||
| this._uniforms.customBumpUvTransform.value.copy(tex.matrix) | |||||
| } | |||||
| updateMaterialDefines({ | |||||
| ...this._defines, | |||||
| ['CUSTOM_BUMP_MAP_ENABLED']: +this.enabled, | |||||
| }, material) | |||||
| }, | |||||
| extraUniforms: { | |||||
| ...this._uniforms, | |||||
| }, | |||||
| computeCacheKey: (material1: PhysicalMaterial) => { | |||||
| return (this.enabled ? '1' : '0') + (material1.userData._hasCustomBump ? '1' : '0') + material1.userData?._customBumpMap?.uuid | |||||
| }, | |||||
| isCompatible: (material1: PhysicalMaterial) => material1.isPhysicalMaterial, | |||||
| getUiConfig: material => { | |||||
| const viewer = this._viewer! | |||||
| const enableCustomBump = this.enableCustomBump.bind(this) | |||||
| const state = material.userData | |||||
| const config: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'CustomBumpMap', | |||||
| onChange: (ev)=>{ | |||||
| if (!ev.config) return | |||||
| this.setDirty() | |||||
| }, | |||||
| children: [ | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Enabled', | |||||
| get value() { | |||||
| return state._hasCustomBump || false | |||||
| }, | |||||
| set value(v) { | |||||
| if (v === state._hasCustomBump) return | |||||
| if (v) { | |||||
| if (!enableCustomBump(material)) | |||||
| viewer.dialog.alert('Cannot add CustomBumpMap.') | |||||
| } else { | |||||
| state._hasCustomBump = false | |||||
| if (material.setDirty) material.setDirty() | |||||
| } | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Bump Scale', | |||||
| bounds: [-1, 1], | |||||
| hidden: () => !state._hasCustomBump, | |||||
| property: [state, '_customBumpScale'], | |||||
| onChange: this.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| label: 'Bump Map', | |||||
| hidden: () => !state._hasCustomBump, | |||||
| property: [state, '_customBumpMap'], | |||||
| onChange: ()=>{ | |||||
| material.setDirty() | |||||
| }, | |||||
| }, | |||||
| makeSamplerUi(state as any, '_customBumpMap'), | |||||
| ], | |||||
| } | |||||
| return config | |||||
| }, | |||||
| } | |||||
| setDirty = (): void => { | |||||
| this.materialExtension.setDirty?.() | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| private _loaderCreate({loader}: {loader: GLTFLoader2}) { | |||||
| if (!loader.isGLTFLoader2) return | |||||
| loader.register((p) => new GLTFMaterialsCustomBumpMapImport(p)) | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this._loaderCreate = this._loaderCreate.bind(this) | |||||
| } | |||||
| onAdded(v: ThreeViewer) { | |||||
| super.onAdded(v) | |||||
| // v.addEventListener('preRender', this._preRender) | |||||
| v.assetManager.materials.registerMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any) | |||||
| v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsCustomBumpMapExport) | |||||
| // v.getPlugin(GBufferPlugin)?.material?.registerMaterialExtensions([this.materialExtension]) | |||||
| } | |||||
| onRemove(v: ThreeViewer) { | |||||
| v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any) | |||||
| const exporter = v.assetManager.exporter.getExporter('gltf', 'glb') | |||||
| if (exporter) { | |||||
| const index = exporter.extensions?.indexOf(glTFMaterialsCustomBumpMapExport) | |||||
| if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1) | |||||
| } | |||||
| // v.getPlugin(GBufferPlugin)?.material?.unregisterMaterialExtensions([this.materialExtension]) | |||||
| return super.onRemove(v) | |||||
| } | |||||
| public static readonly CUSTOM_BUMP_MAP_GLTF_EXTENSION = 'WEBGI_materials_custom_bump_map' | |||||
| } | |||||
| declare module '../../core/IMaterial' { | |||||
| interface IMaterialUserData { | |||||
| _hasCustomBump?: boolean | |||||
| _customBumpMap?: ITexture | null | |||||
| _customBumpScale?: number | |||||
| } | |||||
| } | |||||
| /** | |||||
| * FragmentClipping Materials Extension | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html | |||||
| */ | |||||
| class GLTFMaterialsCustomBumpMapImport implements GLTFLoaderPlugin { | |||||
| public name: string | |||||
| public parser: GLTFParser | |||||
| constructor(parser: GLTFParser) { | |||||
| this.parser = parser | |||||
| this.name = CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION | |||||
| } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[materialIndex] | |||||
| if (!materialDef.extensions || !materialDef.extensions[this.name]) return | |||||
| const extension = materialDef.extensions[this.name] | |||||
| if (!materialParams.userData) materialParams.userData = {} | |||||
| materialParams.userData._hasCustomBump = true // single _ so that its saved when cloning but not when saving | |||||
| materialParams.userData._customBumpScale = extension.customBumpScale ?? 0.0 | |||||
| const pending = [] | |||||
| const tex = extension.customBumpMap | |||||
| if (tex) { | |||||
| pending.push(parser.assignTexture(materialParams.userData, '_customBumpMap', tex).then((t: Texture) => { | |||||
| // t.format = RGBFormat | |||||
| t.colorSpace = SRGBColorSpace | |||||
| })) | |||||
| } | |||||
| return Promise.all(pending) | |||||
| } | |||||
| // do any mesh or geometry processing here | |||||
| // afterRoot(result: GLTF): Promise<void> | null { | |||||
| // result.scene.traverse((object: any) => { | |||||
| // const mat = object.material?.userData?._hasCustomBump | |||||
| // if (!mat) return | |||||
| // const geom = object.geometry | |||||
| // if (!geom.attributes.tangent) { | |||||
| // geom.computeTangents() | |||||
| // geom.attributes.tangent.needsUpdate = true | |||||
| // } | |||||
| // }) | |||||
| // return null | |||||
| // } | |||||
| } | |||||
| const glTFMaterialsCustomBumpMapExport = (w: GLTFWriter2)=> ({ | |||||
| writeMaterial: (material: any, materialDef: any) => { | |||||
| if (!material.isMeshStandardMaterial || !material.userData._hasCustomBump) return | |||||
| if ((material.userData._customBumpScale || 0) < 0.001) return // todo: is this correct? | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| const extensionDef: any = {} | |||||
| extensionDef.customBumpScale = material.userData._customBumpScale || 1.0 | |||||
| if (material.userData._customBumpMap) { | |||||
| const customBumpMapDef = {index: w.processTexture(material.userData._customBumpMap)} | |||||
| w.applyTextureTransform(customBumpMapDef, material.userData._customBumpMap) | |||||
| extensionDef.customBumpMap = customBumpMapDef | |||||
| } | |||||
| materialDef.extensions[ CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION ] = extensionDef | |||||
| w.extensionsUsed[ CustomBumpMapPlugin.CUSTOM_BUMP_MAP_GLTF_EXTENSION ] = true | |||||
| }, | |||||
| }) |
| import {Matrix3, Plane as PlaneThree, Vector4, Vector4Tuple} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' | |||||
| import {serialize} from 'ts-browser-helpers' | |||||
| import {IMaterial, IMaterialUserData, IObject3D, PhysicalMaterial} from '../../core' | |||||
| import {MaterialExtension, updateMaterialDefines} from '../../materials' | |||||
| import {shaderReplaceString, ThreeSerialization} from '../../utils' | |||||
| import {GLTFLoader2, GLTFWriter2} from '../../assetmanager' | |||||
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import FragmentClippingExtensionPluginPars from './shaders/FragmentClippingExtensionPlugin.pars.glsl' | |||||
| import FragmentClippingExtensionPluginPatch from './shaders/FragmentClippingExtensionPlugin.patch.glsl' | |||||
| /** | |||||
| * FragmentClipping Materials Extension | |||||
| * Adds a material extension to PhysicalMaterial to add support for fragment clipping. | |||||
| * Fragment clipping allows to clip fragments of the material in screen space or world space based on a circle, rectangle, plane, sphere, etc. | |||||
| * It uses fixed SDFs with params defined by the user for clipping. | |||||
| * It also adds a UI to the material to edit the settings. | |||||
| * It uses WEBGI_materials_fragment_clipping_extension glTF extension to save the settings in glTF files. | |||||
| * @category Plugins | |||||
| */ | |||||
| @uiFolderContainer('FragmentClipping Materials') | |||||
| export class FragmentClippingExtensionPlugin extends AViewerPluginSync<''> { | |||||
| static readonly PluginType = 'FragmentClippingExtensionPlugin1' | |||||
| @uiToggle('Enabled', (that: FragmentClippingExtensionPlugin)=>({onChange: that.setDirty})) | |||||
| @serialize() enabled = true | |||||
| private _defines: any = { | |||||
| ['FRAG_CLIPPING_DEBUG']: 0, | |||||
| } | |||||
| private _uniforms: any = { | |||||
| fragClippingPosition: {value: new Vector4()}, // point on plane, center of sphere, center of cylinder, etc | |||||
| fragClippingParams: {value: new Vector4()}, // normal of plane, radius of sphere, radius of cylinder, etc | |||||
| fragClippingCamAspect: {value: 1}, | |||||
| } | |||||
| public static AddFragmentClipping(material: IMaterial, params?: IMaterialUserData['_fragmentClippingExt']): boolean { | |||||
| const ud = material?.userData | |||||
| if (!ud) return false | |||||
| if (!ud._fragmentClippingExt) { | |||||
| ud._fragmentClippingExt = {} | |||||
| } | |||||
| const tf = ud._fragmentClippingExt | |||||
| tf.clipEnabled = true | |||||
| if (tf.clipPosition === undefined) tf.clipPosition = [0, 0, 0, 0] | |||||
| if (tf.clipParams === undefined) tf.clipParams = [0, 0, 0, 0] | |||||
| if (tf.clipMode === undefined !== undefined) tf.clipMode = FragmentClippingMode.Circle | |||||
| if (tf.clipInvert === undefined !== undefined) tf.clipInvert = false | |||||
| Object.assign(tf, params) | |||||
| if (material.setDirty) material.setDirty() | |||||
| return true | |||||
| } | |||||
| private _plane = new PlaneThree() | |||||
| private _viewNormalMatrix = new Matrix3() | |||||
| private _v4 = new Vector4() | |||||
| readonly materialExtension: MaterialExtension = { | |||||
| parsFragmentSnippet: (_, material: PhysicalMaterial)=>{ | |||||
| if (!this.enabled || !material?.userData._fragmentClippingExt?.clipEnabled) return '' | |||||
| return Object.entries(FragmentClippingMode) | |||||
| .map(v=>['FragmentClippingMode.' + v[0], '' + v[1]])// replace enum with integer values in the shader | |||||
| .reduce((a, v)=>a.replace(v[0], v[1]), FragmentClippingExtensionPluginPars) | |||||
| }, | |||||
| shaderExtender: (shader, material: PhysicalMaterial) => { | |||||
| if (!this.enabled || !material?.userData._fragmentClippingExt?.clipEnabled) return | |||||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker mainStart', Object.entries(FragmentClippingMode) | |||||
| .map(v=>['FragmentClippingMode.' + v[0], '' + v[1]]) // replace enum with integer values in the shader | |||||
| .reduce((a, v)=>a.replace(v[0], v[1]), '\n' + FragmentClippingExtensionPluginPatch), {append: true}) | |||||
| }, | |||||
| onObjectRender: (object: IObject3D, material) => { | |||||
| let tfUd = material.userData._fragmentClippingExt | |||||
| if (material.userData.isGBufferMaterial && object && object.material && !Array.isArray(object.material)) { // todo isGBufferMaterial | |||||
| tfUd = object.material?.userData._fragmentClippingExt | |||||
| } | |||||
| if (!tfUd?.clipEnabled) return | |||||
| if (Array.isArray(tfUd.clipPosition)) | |||||
| this._uniforms.fragClippingPosition.value.fromArray(tfUd.clipPosition) | |||||
| else | |||||
| this._uniforms.fragClippingPosition.value.copy(tfUd.clipPosition) | |||||
| if (tfUd.clipMode === FragmentClippingMode.Plane && tfUd.clipParams) { | |||||
| const clipParams = Array.isArray(tfUd.clipParams) ? this._v4.fromArray(tfUd.clipParams) : this._v4.copy(tfUd.clipParams) | |||||
| const viewMatrix = this._viewer!.scene.mainCamera.matrixWorldInverse | |||||
| this._plane.normal.set(clipParams.x, clipParams.y, clipParams.z) | |||||
| this._plane.constant = clipParams.w | |||||
| this._viewNormalMatrix.getNormalMatrix(viewMatrix) | |||||
| this._plane.applyMatrix4(viewMatrix, this._viewNormalMatrix) | |||||
| this._uniforms.fragClippingParams.value.set(this._plane.normal.x, this._plane.normal.y, this._plane.normal.z, this._plane.constant) | |||||
| } else { | |||||
| if (Array.isArray(tfUd.clipPosition)) | |||||
| this._uniforms.fragClippingParams.value.fromArray(tfUd.clipParams) | |||||
| else | |||||
| this._uniforms.fragClippingParams.value.copy(tfUd.clipParams) | |||||
| } | |||||
| if (this._viewer?.scene.mainCamera.isPerspectiveCamera) | |||||
| this._uniforms.fragClippingCamAspect.value = this._viewer?.scene.mainCamera.aspect | |||||
| else this._uniforms.fragClippingCamAspect.value = 1.0 | |||||
| updateMaterialDefines({ | |||||
| ...this._defines, | |||||
| // ['FRAGMENT_CLIPPING_EXTENSION_ENABLED']: this.enabled, | |||||
| ['FRAG_CLIPPING_MODE']: +(tfUd.clipMode ?? FragmentClippingMode.Circle), | |||||
| ['FRAG_CLIPPING_INVERSE']: +(tfUd.clipInvert ?? false), | |||||
| }, material) | |||||
| }, | |||||
| extraUniforms: { | |||||
| ...this._uniforms, | |||||
| }, | |||||
| computeCacheKey: (material1: PhysicalMaterial) => { | |||||
| return (this.enabled ? '1' : '0') + (material1.userData._fragmentClippingExt?.clipEnabled ? '1' : '0') | |||||
| }, | |||||
| isCompatible: (material1: PhysicalMaterial) => { | |||||
| return material1.isPhysicalMaterial || material1.userData.isGBufferMaterial // todo isGBufferMaterial | |||||
| }, | |||||
| getUiConfig: material => { | |||||
| const viewer = this._viewer! | |||||
| if (material.userData._fragmentClippingExt === undefined) material.userData._fragmentClippingExt = {} | |||||
| const state = material.userData._fragmentClippingExt | |||||
| const config: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Fragment Clipping', | |||||
| onChange: (ev)=>{ | |||||
| if (!ev.config) return | |||||
| this.setDirty() | |||||
| }, | |||||
| children: [ | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Enabled', | |||||
| get value() { | |||||
| return state.clipEnabled || false | |||||
| }, | |||||
| set value(v) { | |||||
| if (v === state.clipEnabled) return | |||||
| if (v) { | |||||
| if (!FragmentClippingExtensionPlugin.AddFragmentClipping(material)) | |||||
| viewer.dialog.alert('Cannot add FragmentClippingExtension.') | |||||
| } else { | |||||
| state.clipEnabled = false | |||||
| if (material.setDirty) material.setDirty() | |||||
| } | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'dropdown', | |||||
| label: 'Mode', | |||||
| children: Object.entries(FragmentClippingMode) | |||||
| // .filter(key => !isNaN(Number(FragmentClippingMode[key]))) | |||||
| .map(v => ({label: v[0], value: v[1]})), | |||||
| hidden: () => !state.clipEnabled, | |||||
| property: [state, 'clipMode'], | |||||
| }, | |||||
| { | |||||
| type: 'vec4', | |||||
| label: 'Position', | |||||
| bounds: [-1, 1], | |||||
| hidden: () => !state.clipEnabled, | |||||
| property: [state, 'clipPosition'], | |||||
| }, | |||||
| { | |||||
| type: 'vec4', | |||||
| label: 'Params', | |||||
| bounds: [0, 1], | |||||
| hidden: () => !state.clipEnabled, | |||||
| property: [state, 'clipParams'], | |||||
| }, | |||||
| { | |||||
| type: 'toggle', | |||||
| label: 'Invert', | |||||
| hidden: () => !state.clipEnabled, | |||||
| property: [state, 'clipInvert'], | |||||
| }, | |||||
| ], | |||||
| } | |||||
| return config | |||||
| }, | |||||
| } | |||||
| setDirty = (): void => { | |||||
| this.materialExtension.setDirty?.() | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| private _loaderCreate({loader}: {loader: GLTFLoader2}) { | |||||
| if (!loader.isGLTFLoader2) return | |||||
| loader.register((p) => new GLTFMaterialsFragmentClippingExtensionImport(p)) | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this._loaderCreate = this._loaderCreate.bind(this) | |||||
| } | |||||
| onAdded(v: ThreeViewer) { | |||||
| super.onAdded(v) | |||||
| // v.addEventListener('preRender', this._preRender) | |||||
| v.assetManager.materials.registerMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any) | |||||
| v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsFragmentClippingExtensionExport) | |||||
| // v.getPlugin(GBufferPlugin)?.material?.registerMaterialExtensions([this.materialExtension]) | |||||
| } | |||||
| onRemove(v: ThreeViewer) { | |||||
| v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any) | |||||
| const exporter = v.assetManager.exporter.getExporter('gltf', 'glb') | |||||
| if (exporter) { | |||||
| const index = exporter.extensions?.indexOf(glTFMaterialsFragmentClippingExtensionExport) | |||||
| if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1) | |||||
| } | |||||
| // v.getPlugin(GBufferPlugin)?.material?.unregisterMaterialExtensions([this.materialExtension]) | |||||
| return super.onRemove(v) | |||||
| } | |||||
| public static readonly FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION = 'WEBGI_materials_fragment_clipping_extension' | |||||
| } | |||||
| declare module '../../core/IMaterial' { | |||||
| interface IMaterialUserData { | |||||
| _fragmentClippingExt?: { | |||||
| clipEnabled?: boolean | |||||
| clipPosition?: Vector4|Vector4Tuple | |||||
| clipParams?: Vector4|Vector4Tuple | |||||
| clipMode?: FragmentClippingMode | |||||
| clipInvert?: boolean | |||||
| } | |||||
| } | |||||
| } | |||||
| export enum FragmentClippingMode { | |||||
| Circle = 0, | |||||
| Ellipse = 1, | |||||
| Rectangle = 2, | |||||
| Plane = 3, | |||||
| Sphere = 4 | |||||
| } | |||||
| /** | |||||
| * FragmentClipping Materials Extension | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html | |||||
| */ | |||||
| class GLTFMaterialsFragmentClippingExtensionImport implements GLTFLoaderPlugin { | |||||
| public name: string | |||||
| public parser: GLTFParser | |||||
| constructor(parser: GLTFParser) { | |||||
| this.parser = parser | |||||
| this.name = FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION | |||||
| } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[materialIndex] | |||||
| if (!materialDef.extensions || !materialDef.extensions[this.name]) return | |||||
| const extension = materialDef.extensions[this.name] | |||||
| if (!materialParams.userData) materialParams.userData = {} | |||||
| FragmentClippingExtensionPlugin.AddFragmentClipping(materialParams) | |||||
| ThreeSerialization.Deserialize(extension, materialParams.userData._fragmentClippingExt) | |||||
| } | |||||
| } | |||||
| const glTFMaterialsFragmentClippingExtensionExport = (w: GLTFWriter2)=> ({ | |||||
| writeMaterial: (material: any, materialDef: any) => { | |||||
| if (!material.isMeshStandardMaterial || !material.userData._fragmentClippingExt?.clipEnabled) return | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| const extensionDef: any = ThreeSerialization.Serialize(material.userData._fragmentClippingExt) | |||||
| materialDef.extensions[ FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION ] = extensionDef | |||||
| w.extensionsUsed[ FragmentClippingExtensionPlugin.FRAGMENT_CLIPPING_EXTENSION_GLTF_EXTENSION ] = true | |||||
| }, | |||||
| }) |
| import {Vector2, Vector2Tuple, Vector3, Vector3Tuple, Vector4, Vector4Tuple} from 'three' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' | |||||
| import {serialize} from 'ts-browser-helpers' | |||||
| import {IMaterial, IMaterialUserData, IObject3D, PhysicalMaterial} from '../../core' | |||||
| import {MaterialExtension, updateMaterialDefines} from '../../materials' | |||||
| import {shaderReplaceString, ThreeSerialization} from '../../utils' | |||||
| import {GLTFLoader2, GLTFWriter2} from '../../assetmanager' | |||||
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import NoiseBumpMaterialPluginPars from './shaders/NoiseBumpMaterialPlugin.pars.glsl' | |||||
| import NoiseBumpMaterialPluginPatch from './shaders/NoiseBumpMaterialPlugin.patch.glsl' | |||||
| /** | |||||
| * NoiseBump Materials Extension | |||||
| * Adds a material extension to PhysicalMaterial to add support for sparkle bump / noise bump by creating procedural bump map from noise to simulate sparkle flakes. | |||||
| * It uses voronoise function from blender along with several additions to generate the noise for the generation. | |||||
| * It also adds a UI to the material to edit the settings. | |||||
| * It uses WEBGI_materials_noise_bump glTF extension to save the settings in glTF files. | |||||
| * @category Plugins | |||||
| */ | |||||
| @uiFolderContainer('NoiseBump Materials') | |||||
| export class NoiseBumpMaterialPlugin extends AViewerPluginSync<''> { | |||||
| static readonly PluginType = 'NoiseBumpMaterialPlugin' | |||||
| @uiToggle('Enabled', (that: NoiseBumpMaterialPlugin)=>({onChange: that.setDirty})) | |||||
| @serialize() enabled = true | |||||
| // private _defines: any = { | |||||
| // } | |||||
| private _uniforms: any = { | |||||
| noiseBumpParams: {value: new Vector2()}, // u scale, v scale, | |||||
| noiseBumpScale: {value: 0.05}, | |||||
| noiseBumpFlakeScale: {value: 1000.0}, | |||||
| noiseFlakeClamp: {value: 1.0}, | |||||
| noiseFlakeRadius: {value: 0.5}, | |||||
| flakeParams: {value: new Vector4(0, 1, 3, 0)}, | |||||
| flakeFallOffParams: {value: new Vector3(0, 1, 0)}, | |||||
| useColorFlakes: {value: false}, | |||||
| } | |||||
| public static AddNoiseBumpMaterial(material: IMaterial, params?: IMaterialUserData['_noiseBumpMat']): boolean { | |||||
| const ud = material?.userData | |||||
| if (!ud) return false | |||||
| if (!ud._noiseBumpMat) { | |||||
| ud._noiseBumpMat = {} | |||||
| } | |||||
| const tf = ud._noiseBumpMat | |||||
| tf.hasBump = true | |||||
| if (tf.bumpNoiseParams === undefined) tf.bumpNoiseParams = new Vector2(0.5, 0.5) | |||||
| if (tf.bumpScale === undefined) tf.bumpScale = 0.05 | |||||
| if (tf.flakeScale === undefined) tf.flakeScale = 0.05 | |||||
| if (tf.flakeClamp === undefined) tf.flakeClamp = 1 | |||||
| if (tf.flakeRadius === undefined) tf.flakeRadius = 0.3 | |||||
| if (tf.useColorFlakes === undefined) tf.useColorFlakes = false | |||||
| if (tf.flakeParams === undefined) tf.flakeParams = new Vector4(0, 1, 3, 0) | |||||
| if (tf.flakeFallOffParams === undefined) tf.flakeFallOffParams = new Vector3(0, 1, 0) | |||||
| Object.assign(tf, params) | |||||
| if (material.setDirty) material.setDirty() | |||||
| return true | |||||
| } | |||||
| readonly materialExtension: MaterialExtension = { | |||||
| parsFragmentSnippet: (_, material: PhysicalMaterial)=>{ | |||||
| if (!this.enabled || !material?.userData._noiseBumpMat?.hasBump) return '' | |||||
| return NoiseBumpMaterialPluginPars | |||||
| }, | |||||
| shaderExtender: (shader, material: PhysicalMaterial) => { | |||||
| if (!this.enabled || !material?.userData._noiseBumpMat?.hasBump) return | |||||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker beforeAccumulation', NoiseBumpMaterialPluginPatch, {prepend: true}) | |||||
| ;(shader as any).defines.USE_UV = '' | |||||
| ;(shader as any).extensionDerivatives = true | |||||
| }, | |||||
| onObjectRender: (_: IObject3D, material) => { | |||||
| const tfUd = material.userData._noiseBumpMat | |||||
| if (!tfUd?.hasBump) return | |||||
| if (Array.isArray(tfUd.bumpNoiseParams)) this._uniforms.noiseBumpParams.value.fromArray(tfUd.bumpNoiseParams) | |||||
| else this._uniforms.noiseBumpParams.value.copy(tfUd.bumpNoiseParams) | |||||
| this._uniforms.noiseBumpScale.value = tfUd.bumpScale | |||||
| this._uniforms.noiseBumpFlakeScale.value = tfUd.flakeScale | |||||
| this._uniforms.noiseFlakeClamp.value = tfUd.flakeClamp | |||||
| this._uniforms.noiseFlakeRadius.value = tfUd.flakeRadius | |||||
| if (Array.isArray(tfUd.flakeParams)) this._uniforms.flakeParams.value.fromArray(tfUd.flakeParams) | |||||
| else this._uniforms.flakeParams.value.copy(tfUd.flakeParams) | |||||
| if (Array.isArray(tfUd.flakeFallOffParams)) this._uniforms.flakeFallOffParams.value.fromArray(tfUd.flakeFallOffParams) | |||||
| else this._uniforms.flakeFallOffParams.value.copy(tfUd.flakeFallOffParams) | |||||
| this._uniforms.useColorFlakes.value = tfUd.useColorFlakes | |||||
| updateMaterialDefines({ | |||||
| // ...this._defines, | |||||
| ['NOISE_BUMP_MATERIAL_ENABLED']: +this.enabled, | |||||
| }, material) | |||||
| }, | |||||
| extraUniforms: { | |||||
| ...this._uniforms, | |||||
| }, | |||||
| computeCacheKey: (material1: PhysicalMaterial) => { | |||||
| return (this.enabled ? '1' : '0') + (material1.userData._noiseBumpMat?.hasBump ? '1' : '0') | |||||
| }, | |||||
| isCompatible: (material1: PhysicalMaterial) => material1.isPhysicalMaterial, | |||||
| getUiConfig: material => { | |||||
| const viewer = this._viewer! | |||||
| if (material.userData._noiseBumpMat === undefined) material.userData._noiseBumpMat = {} | |||||
| const state = material.userData._noiseBumpMat | |||||
| const config: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'SparkleBump (NoiseBump)', | |||||
| onChange: (ev)=>{ | |||||
| if (!ev.config) return | |||||
| this.setDirty() | |||||
| }, | |||||
| children: [ | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Enabled', | |||||
| get value() { | |||||
| return state.hasBump || false | |||||
| }, | |||||
| set value(v) { | |||||
| if (v === state.hasBump) return | |||||
| if (v) { | |||||
| if (!NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(material)) | |||||
| viewer.dialog.alert('Cannot add NoiseBumpMaterial.') | |||||
| } else { | |||||
| state.hasBump = false | |||||
| if (material.setDirty) material.setDirty() | |||||
| } | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'vec4', | |||||
| label: 'Bump Noise Params', | |||||
| bounds: [0, 1], | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state, 'bumpNoiseParams'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Bump Scale', | |||||
| bounds: [0, 0.001], | |||||
| stepSize: 0.00001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state, 'bumpScale'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Scale', | |||||
| bounds: [100, 10000], | |||||
| stepSize: 0.0001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state, 'flakeScale'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Clamp', | |||||
| bounds: [0, 1], | |||||
| stepSize: 1, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state, 'flakeClamp'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Radius', | |||||
| bounds: [0.01, 1], | |||||
| stepSize: 0.001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state, 'flakeRadius'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Roughness', | |||||
| bounds: [0., 1], | |||||
| stepSize: 0.01, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeParams, 'x'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Metalness', | |||||
| bounds: [0., 1], | |||||
| stepSize: 0.01, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeParams, 'y'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Strength', | |||||
| bounds: [0.0, 100], | |||||
| stepSize: 0.001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeParams, 'z'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Flake Threshold', | |||||
| bounds: [0.1, 10], | |||||
| stepSize: 0.001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeParams, 'w'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Falloff', | |||||
| stepSize: 1, | |||||
| bounds: [0, 1], | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeFallOffParams, 'x'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Linear falloff factor', | |||||
| bounds: [0., 10], | |||||
| stepSize: 0.001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeFallOffParams, 'y'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Quadratic falloff factor', | |||||
| bounds: [0., 10], | |||||
| stepSize: 0.001, | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state.flakeFallOffParams, 'z'], | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Colored Flakes', | |||||
| hidden: () => !state.hasBump, | |||||
| property: [state, 'useColorFlakes'], | |||||
| }, | |||||
| ], | |||||
| } | |||||
| return config | |||||
| }, | |||||
| } | |||||
| setDirty = (): void => { | |||||
| this.materialExtension.setDirty?.() | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| private _loaderCreate({loader}: {loader: GLTFLoader2}) { | |||||
| if (!loader.isGLTFLoader2) return | |||||
| loader.register((p) => new GLTFMaterialsNoiseBumpMaterialImport(p)) | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this._loaderCreate = this._loaderCreate.bind(this) | |||||
| } | |||||
| onAdded(v: ThreeViewer) { | |||||
| super.onAdded(v) | |||||
| v.assetManager.materials.registerMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer.addEventListener('loaderCreate', this._loaderCreate as any) | |||||
| v.assetManager.exporter.getExporter('gltf', 'glb')?.extensions?.push(glTFMaterialsNoiseBumpMaterialExport) | |||||
| } | |||||
| onRemove(v: ThreeViewer) { | |||||
| v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension) | |||||
| v.assetManager.importer?.removeEventListener('loaderCreate', this._loaderCreate as any) | |||||
| const exporter = v.assetManager.exporter.getExporter('gltf', 'glb') | |||||
| if (exporter) { | |||||
| const index = exporter.extensions?.indexOf(glTFMaterialsNoiseBumpMaterialExport) | |||||
| if (index !== undefined && index >= 0) exporter.extensions?.splice(index, 1) | |||||
| } | |||||
| return super.onRemove(v) | |||||
| } | |||||
| public static readonly NOISE_BUMP_MATERIAL_GLTF_EXTENSION = 'WEBGI_materials_noise_bump' | |||||
| } | |||||
| declare module '../../core/IMaterial' { | |||||
| interface IMaterialUserData { | |||||
| _noiseBumpMat?: { | |||||
| hasBump?: boolean | |||||
| bumpNoiseParams?: Vector2Tuple | Vector2 | |||||
| bumpScale?: number | |||||
| flakeScale?: number | |||||
| flakeClamp?: number | |||||
| flakeRadius?: number | |||||
| useColorFlakes?: boolean | |||||
| flakeParams?: Vector4Tuple | Vector4 | |||||
| flakeFallOffParams?: Vector3Tuple | Vector3 | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * FragmentClipping Materials Extension | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html | |||||
| */ | |||||
| class GLTFMaterialsNoiseBumpMaterialImport implements GLTFLoaderPlugin { | |||||
| public name: string | |||||
| public parser: GLTFParser | |||||
| constructor(parser: GLTFParser) { | |||||
| this.parser = parser | |||||
| this.name = NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION | |||||
| } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[materialIndex] | |||||
| if (!materialDef.extensions || !materialDef.extensions[this.name]) return | |||||
| const extension = materialDef.extensions[this.name] | |||||
| if (!materialParams.userData) materialParams.userData = {} | |||||
| NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(materialParams) | |||||
| ThreeSerialization.Deserialize(extension, materialParams.userData._noiseBumpMat) | |||||
| } | |||||
| } | |||||
| const glTFMaterialsNoiseBumpMaterialExport = (w: GLTFWriter2)=> ({ | |||||
| writeMaterial: (material: any, materialDef: any) => { | |||||
| if (!material.isMeshStandardMaterial || !material.userData._noiseBumpMat?.hasBump) return | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| const extensionDef: any = ThreeSerialization.Serialize(material.userData._noiseBumpMat) | |||||
| materialDef.extensions[ NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION ] = extensionDef | |||||
| w.extensionsUsed[ NoiseBumpMaterialPlugin.NOISE_BUMP_MATERIAL_GLTF_EXTENSION ] = true | |||||
| }, | |||||
| }) |
| #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 | |||||
| #if CUSTOM_BUMP_MAP_BICUBIC > 0 // from http://www.java-gaming.org/index.php?topic=35123.0 | |||||
| vec4 cubic_cb(float v){ | |||||
| vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v; | |||||
| vec4 s = n * n * n; | |||||
| float x = s.x; | |||||
| float y = s.y - 4.0 * s.x; | |||||
| float z = s.z - 4.0 * s.y + 6.0 * s.x; | |||||
| float w = 6.0 - x - y - z; | |||||
| return vec4(x, y, z, w) * (1.0/6.0); | |||||
| } | |||||
| vec4 textureBicubic_cb(sampler2D sampler, vec2 texCoords){ | |||||
| vec2 texSize = vec2(textureSize(sampler, 0)); | |||||
| vec2 invTexSize = 1.0 / texSize; | |||||
| texCoords = texCoords * texSize - 0.5; | |||||
| vec2 fxy = fract(texCoords); | |||||
| texCoords -= fxy; | |||||
| vec4 xcubic = cubic_cb(fxy.x); | |||||
| vec4 ycubic = cubic_cb(fxy.y); | |||||
| vec4 c = texCoords.xxyy + vec2 (-0.5, +1.5).xyxy; | |||||
| vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw); | |||||
| vec4 offset = c + vec4 (xcubic.yw, ycubic.yw) / s; | |||||
| offset *= invTexSize.xxyy; | |||||
| vec4 sample0 = texture(sampler, offset.xz); | |||||
| vec4 sample1 = texture(sampler, offset.yz); | |||||
| vec4 sample2 = texture(sampler, offset.xw); | |||||
| vec4 sample3 = texture(sampler, offset.yw); | |||||
| float sx = s.x / (s.x + s.y); | |||||
| float sy = s.z / (s.z + s.w); | |||||
| return mix( | |||||
| mix(sample3, sample2, sx), mix(sample1, sample0, sx) | |||||
| , sy); | |||||
| } | |||||
| #endif | |||||
| varying vec2 vCustomBumpUv; | |||||
| uniform sampler2D customBumpMap; | |||||
| uniform float customBumpScale; | |||||
| // same as bumpmap_pars_fragment, but with customBumpMap, customBumpUv and bicubic | |||||
| vec2 dHdxy_fwd_cb() { | |||||
| vec2 dSTdx = dFdx( vCustomBumpUv ); | |||||
| vec2 dSTdy = dFdy( vCustomBumpUv ); | |||||
| #if CUSTOM_BUMP_MAP_BICUBIC > 0 | |||||
| float Hll = customBumpScale * textureBicubic_cb( customBumpMap, vCustomBumpUv ).x; | |||||
| float dBx = customBumpScale * textureBicubic_cb( customBumpMap, vCustomBumpUv + dSTdx ).x - Hll; | |||||
| float dBy = customBumpScale * textureBicubic_cb( customBumpMap, vCustomBumpUv + dSTdy ).x - Hll; | |||||
| #else | |||||
| float Hll = customBumpScale * texture2D( customBumpMap, vCustomBumpUv ).x; | |||||
| float dBx = customBumpScale * texture2D( customBumpMap, vCustomBumpUv + dSTdx ).x - Hll; | |||||
| float dBy = customBumpScale * texture2D( customBumpMap, vCustomBumpUv + dSTdy ).x - Hll; | |||||
| #endif | |||||
| return vec2( dBx, dBy ); | |||||
| } | |||||
| #ifndef USE_BUMPMAP | |||||
| vec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) { | |||||
| vec3 vSigmaX = dFdx( surf_pos.xyz ); | |||||
| vec3 vSigmaY = dFdy( surf_pos.xyz ); | |||||
| vec3 vN = surf_norm; // normalized | |||||
| vec3 R1 = cross( vSigmaY, vN ); | |||||
| vec3 R2 = cross( vN, vSigmaX ); | |||||
| float fDet = dot( vSigmaX, R1 ) * faceDirection; | |||||
| vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 ); | |||||
| return normalize( abs( fDet ) * surf_norm - vGrad ); | |||||
| } | |||||
| #endif | |||||
| #endif |
| #include <simpleCameraHelpers> | |||||
| uniform vec4 fragClippingPosition; | |||||
| uniform vec4 fragClippingParams; | |||||
| uniform float fragClippingCamAspect; | |||||
| #if FRAG_CLIPPING_MODE == FragmentClippingMode.Circle | |||||
| float fragClippingCircle(){ | |||||
| vec2 pos = viewToScreen(vViewPosition.xyz).xy; | |||||
| float radius = fragClippingParams.x; | |||||
| vec2 center = fragClippingPosition.xy; | |||||
| pos.y /= fragClippingCamAspect; | |||||
| center.y /= fragClippingCamAspect; | |||||
| return length(pos - center) - radius; | |||||
| } | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Ellipse | |||||
| float fragClippingEllipse(){ | |||||
| vec2 pos = viewToScreen(vViewPosition.xyz).xy; | |||||
| vec2 radius = fragClippingParams.xy; | |||||
| vec2 center = fragClippingPosition.xy; | |||||
| pos.y /= fragClippingCamAspect; | |||||
| center.y /= fragClippingCamAspect; | |||||
| return length((pos - center) / radius) - 1.0; | |||||
| } | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Rectangle | |||||
| float fragClippingRectangle(){ | |||||
| vec2 pos = viewToScreen(vViewPosition.xyz).xy; | |||||
| vec2 radius = fragClippingParams.xy; | |||||
| vec2 center = fragClippingPosition.xy; | |||||
| pos.y /= fragClippingCamAspect; | |||||
| center.y /= fragClippingCamAspect; | |||||
| vec2 d = abs(pos - center) - radius; | |||||
| return min(max(d.x,d.y),0.0) + length(max(d,0.0)); | |||||
| } | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Plane | |||||
| float fragClippingPlane(){ | |||||
| vec3 pos = vViewPosition.xyz; | |||||
| vec3 normal = fragClippingParams.xyz; | |||||
| float d = dot(pos, normal) - fragClippingParams.w; | |||||
| return d; | |||||
| } | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Sphere | |||||
| float fragClippingSphere(){ | |||||
| vec3 pos = vViewPosition.xyz; | |||||
| vec3 center = fragClippingPosition.xyz; | |||||
| float radius = fragClippingParams.x; | |||||
| pos.y /= fragClippingCamAspect; | |||||
| center.y /= fragClippingCamAspect; | |||||
| return length(pos - center) - radius; | |||||
| } | |||||
| #endif |
| float fragClippingDist = 0.0; | |||||
| #if FRAG_CLIPPING_MODE == FragmentClippingMode.Circle | |||||
| fragClippingDist = fragClippingCircle(); | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Ellipse | |||||
| fragClippingDist = fragClippingEllipse(); | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Rectangle | |||||
| fragClippingDist = fragClippingRectangle(); | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Plane | |||||
| fragClippingDist = fragClippingPlane(); | |||||
| #elif FRAG_CLIPPING_MODE == FragmentClippingMode.Sphere | |||||
| fragClippingDist = fragClippingSphere(); | |||||
| #endif | |||||
| #if FRAG_CLIPPING_DEBUG | |||||
| gl_FragColor = vec4(max(fragClippingDist, 0.0), 0.0, 0.0, 1.0); | |||||
| // gl_FragColor = vec4(vViewPosition.xyz, 1.0); | |||||
| #include <encodings_fragment> | |||||
| return; | |||||
| #endif | |||||
| #if FRAG_CLIPPING_INVERSE == 1 | |||||
| if (fragClippingDist > 0.0) discard; | |||||
| #else | |||||
| if (fragClippingDist < 0.0) discard; | |||||
| #endif |
| #include <randomHelpers> | |||||
| #include <voronoiNoise> | |||||
| uniform vec2 noiseBumpParams; | |||||
| uniform float noiseBumpScale; | |||||
| uniform float noiseBumpFlakeScale; | |||||
| uniform float noiseFlakeClamp; | |||||
| uniform float noiseFlakeRadius; | |||||
| uniform bool useColorFlakes; | |||||
| uniform vec4 flakeParams; // Roughness, Metalness, Strength, Threshold | |||||
| uniform vec3 flakeFallOffParams; // useFallOff, fallOffFactor | |||||
| vec3 perturbNormalArb_nb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) { | |||||
| vec3 vSigmaX = dFdx( surf_pos.xyz ); | |||||
| vec3 vSigmaY = dFdy( surf_pos.xyz ); | |||||
| vec3 vN = surf_norm; // normalized | |||||
| vec3 R1 = cross( vSigmaY, vN ); | |||||
| vec3 R2 = cross( vN, vSigmaX ); | |||||
| float fDet = dot( vSigmaX, R1 ) * faceDirection; | |||||
| vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 ); | |||||
| return normalize( abs( fDet ) * surf_norm - vGrad ); | |||||
| } |
| vec3 outColor, outColor1, outColor2, outColor3, outColor4, outColor5; | |||||
| float distFac = length(vViewPosition.xyz); | |||||
| /*float e = floor( log2( 0.3 * distFac + 3.0 ) / 0.3785116); | |||||
| float level_z = 0.1 * pow( 1.3 , e ) - 0.2;*/ | |||||
| float level = 1.;//0.15 / level_z; | |||||
| vec2 uvMod = noiseBumpFlakeScale * noiseBumpParams.xy * vUv * level; | |||||
| float voronoiDist = clamp(voronoi_f1_2d( uvMod, 1., noiseFlakeClamp, noiseFlakeRadius, outColor ), 0.0, 1.0); | |||||
| vec3 oldNormal = normal; | |||||
| normal = perturbNormalArb_nb( - vViewPosition, normal, (2. * outColor.xy - 1.) * noiseBumpScale, faceDirection ); | |||||
| float oldRoughnessFactor = roughnessFactor; | |||||
| float oldMetalnessFactor = metalnessFactor; | |||||
| roughnessFactor = mix(roughnessFactor, flakeParams.x, 1. - voronoiDist); | |||||
| metalnessFactor = mix(metalnessFactor, flakeParams.y, 1. - voronoiDist); | |||||
| #if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) | |||||
| vec3 sparkleRadiance = getIBLRadiance( normalize(vViewPosition), normal, roughnessFactor ); | |||||
| float sparkleIntensity = length(sparkleRadiance); | |||||
| float sparkleIntensityMultiplier = sparkleIntensity > 1.3 ? flakeParams.z : 1.; | |||||
| vec3 oldDiffuseColor = diffuseColor.rgb; | |||||
| vec2 cellPosition_ = floor(uvMod); | |||||
| vec3 colorRGB = useColorFlakes ? hash3(cellPosition_) : vec3(1.); | |||||
| float fallOff_ = mix(1., 1. / (1. + flakeFallOffParams.y * distFac + flakeFallOffParams.z * distFac * distFac), flakeFallOffParams.x); | |||||
| diffuseColor.rgb *= mix(vec3(1.), sparkleIntensityMultiplier * colorRGB * fallOff_, vec3(1. - voronoiDist)); | |||||
| if(sparkleIntensity < flakeParams.w) { | |||||
| float mixFactor = 1.; | |||||
| roughnessFactor = mix(roughnessFactor, oldRoughnessFactor, mixFactor); | |||||
| metalnessFactor = mix(metalnessFactor, oldMetalnessFactor, mixFactor); | |||||
| normal = normalize(mix(normal, oldNormal, mixFactor)); | |||||
| diffuseColor.rgb = mix(diffuseColor.rgb, oldDiffuseColor, mixFactor); | |||||
| } | |||||
| #endif |