| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,44 @@ | |||
| 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) | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,41 @@ | |||
| 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) | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,49 @@ | |||
| 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) | |||
| @@ -0,0 +1,36 @@ | |||
| <!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> | |||
| @@ -0,0 +1,42 @@ | |||
| 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) | |||
| @@ -42,5 +42,11 @@ export {GLTFAnimationPlugin} from './animation/GLTFAnimationPlugin' | |||
| export {PopmotionPlugin} from './animation/PopmotionPlugin' | |||
| 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 | |||
| export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' | |||
| @@ -0,0 +1,244 @@ | |||
| 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 | |||
| }, | |||
| }) | |||
| @@ -0,0 +1,304 @@ | |||
| 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 | |||
| }, | |||
| }) | |||
| @@ -0,0 +1,281 @@ | |||
| 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 | |||
| }, | |||
| }) | |||
| @@ -0,0 +1,327 @@ | |||
| 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 | |||
| }, | |||
| }) | |||
| @@ -0,0 +1,88 @@ | |||
| #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 | |||
| @@ -0,0 +1,49 @@ | |||
| #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 | |||
| @@ -0,0 +1,24 @@ | |||
| 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 | |||
| @@ -0,0 +1,27 @@ | |||
| #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 ); | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| 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 | |||