Browse Source

Add ParallaxMappingPlugin and example.

master
Palash Bansal 2 years ago
parent
commit
42beef4ec2
No account linked to committer's email address

+ 36
- 0
README.md View File

@@ -114,6 +114,7 @@ To make changes and run the example, click on the CodePen button on the top righ
- [CustomBumpMapPlugin](#custombumpmapplugin) - Custom Bump Map material extension for PhysicalMaterial
- [ClearcoatTintPlugin](#clearcoattintplugin) - Clearcoat Tint material extension for PhysicalMaterial
- [FragmentClippingExtensionPlugin](#fragmentclippingextensionplugin) - Fragment/SDF Clipping material extension for PhysicalMaterial
- [ParallaxMappingPlugin](#parallaxmappingplugin) - Relief Parallax Bump Mapping extension for PhysicalMaterial
- [HDRiGroundPlugin](#hdrigroundplugin) - Add support for ground projected hdri/skybox to the webgl background shader.
- [VirtualCamerasPlugin](#virtualcamerasplugin) - Add support for rendering virtual cameras before the main one every frame.
- [EditorViewWidgetPlugin](#editorviewwidgetplugin) - Adds an interactive ViewHelper/AxisHelper that syncs with the main camera.
@@ -2923,6 +2924,41 @@ material.userData._clearcoatTint.clipEnabled = false
material.setDirty()
```

## ParallaxMappingPlugin

[//]: # (todo: image)

[Example](https://threepipe.org/examples/#parallax-mapping-plugin/) —
[Source Code](./src/plugins/material/ParallaxMappingPlugin.ts) —
[API Reference](https://threepipe.org/docs/classes/ParallaxMappingPlugin.html)

`ParallaxMappingPlugin` adds a material extension to PhysicalMaterial to add support for [parallax relief mapping](https://en.wikipedia.org/wiki/Relief_mapping_(computer_graphics)). The idea is to walk along a ray that has entered the bumpmap's volume, finding the intersection point of the ray with the bumpmap. [Steep parallax mapping](https://en.wikipedia.org/wiki/Parallax_mapping) and [parallax occlusion mapping](https://en.wikipedia.org/wiki/Parallax_occlusion_mapping) are other common names for these techniques.

To use the plugin, add the plugin to the viewer and use the `bumpMap` in `PhysicalMaterial` normally. The max height is determined by the `bumpScale` in the material. This is assumed to be in world scale.

```typescript
import {ThreeViewer, ParallaxMappingPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const parallaxMapping = viewer.addPluginSync(ParallaxMappingPlugin)

// load or create an object

// set the bump map
object.material.bumpMap = await viewer.load<ITexture>(bumps[0]) || null
// set the bump scale
object.material.bumpScale = 0.1
// setDirty to notify the viewer to update.
object.material.setDirty()
```

### References and related links:

- WebGL implementation by Rabbid76 - [github.com/Rabbid76/graphics-snippets](https://github.com/Rabbid76/graphics-snippets/blob/master/html/technique/parallax_005_parallax_relief_mapping_derivative_tbn.html)
- Lesson on Parallax Occlusion Mapping in GLSL - [http://sunandblackcat.com/tipFullView.php?topicid=28](https://web.archive.org/web/20190128023901/http://sunandblackcat.com/tipFullView.php?topicid=28)
- Learn OpenGL - https://learnopengl.com/Advanced-Lighting/Parallax-Mapping

## HDRiGroundPlugin

[//]: # (todo: image)

+ 36
- 0
examples/parallax-mapping/index.html View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Parallax Bump Mapping (Relief)</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>

+ 94
- 0
examples/parallax-mapping/script.ts View File

@@ -0,0 +1,94 @@
import {
_testFinish,
BoxGeometry,
ITexture,
Mesh,
ParallaxMappingPlugin,
PhysicalMaterial,
SSAAPlugin,
ThreeViewer,
} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
plugins: [new SSAAPlugin(4)],
dropzone: {
addOptions: {
disposeSceneObjects: true,
},
},
})

const parallaxMapping = viewer.addPluginSync(ParallaxMappingPlugin)
console.log(parallaxMapping)

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))

ui.setupPluginUi(ParallaxMappingPlugin, {expanded: true})

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')

const cube = new Mesh(
new BoxGeometry(1, 1, 1),
new PhysicalMaterial({
// roughness: 0,
// metalness: 1,
}))

const maps = [
'https://threejs.org/examples/textures/sprite0.png',
'https://threejs.org/examples/textures/uv_grid_opengl.jpg',
'https://threejs.org/examples/models/svg/style-css-inside-defs.svg',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/lookuptable.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/perlin3_cp.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/perlin4_cp.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/ObjectSheet.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/512x512_Texel_Density_Texture_1.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/toy_box_normal.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/example_1_texture.png',
]
const bumps = [
maps[0],
maps[1],
maps[2],
maps[3],
maps[4],
maps[5],
maps[6],
maps[7],
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/toy_box_disp.png',
'https://cdn.jsdelivr.net/gh/Rabbid76/graphics-snippets/resource/texture/example_1_heightmap.png',
]

cube.material.bumpMap = await viewer.load<ITexture>(bumps[0]) || null
cube.material.map = await viewer.load<ITexture>(maps[0]) || null
cube.material.bumpScale = 0.1
viewer.scene.addObject(cube)
ui.appendChild({
type: 'dropdown',
value: maps[0],
label: 'Bump Texture',
children: ['none', ...maps].map((url: string) => ({
label: url.split('/').pop(),
value: url,
})),
onChange: async(ev) => {
console.log(ev.value)
const url = ev.value
const tex = await viewer.load<ITexture>(url) || null
cube.material.map = tex
const bumpUrl = bumps[maps.indexOf(url)]
const bumpTex = await viewer.load<ITexture>(bumpUrl) || null
cube.material.bumpMap = bumpTex
cube.material.setDirty()
},
})
ui.appendChild(cube.material.uiConfig)

}

init().finally(_testFinish)

+ 1
- 0
src/plugins/index.ts View File

@@ -61,6 +61,7 @@ export {CameraViewPlugin, type CameraViewPluginOptions} from './animation/Camera
export {ClearcoatTintPlugin} from './material/ClearcoatTintPlugin'
export {NoiseBumpMaterialPlugin} from './material/NoiseBumpMaterialPlugin'
export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin'
export {ParallaxMappingPlugin} from './material/ParallaxMappingPlugin'
export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin'

// rendering

+ 115
- 0
src/plugins/material/ParallaxMappingPlugin.ts View File

@@ -0,0 +1,115 @@
import {MaterialExtension, updateMaterialDefines} from '../../materials'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {onChange, serialize} from 'ts-browser-helpers'
import {uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js'
import {shaderReplaceString} from '../../utils'
import {ShaderChunk} from 'three'
import {PhysicalMaterial} from '../../core'
import ParallaxMappingPluginReliefShader from './shaders/ParallaxMappingPlugin.relief.glsl'

/**
* Parallax Mapping Plugin
* Adds a material extension to PhysicalMaterial which parallax mapping to bump map in the material.
* This is a port of Relief Parallax Mapping from [Rabbid76/graphics-snippets](https://github.com/Rabbid76/graphics-snippets/blob/master/html/technique/parallax_005_parallax_relief_mapping_derivative_tbn.html)
* @category Plugins
*/
@uiFolderContainer('Parallax Mapping')
export class ParallaxMappingPlugin extends AViewerPluginSync<''> {
public static PluginType = 'ReliefParallaxMapping'

@onChange(ParallaxMappingPlugin.prototype._updateExtension)
@serialize()
@uiToggle('Enabled') enabled = true

@uiSlider('Step count', [1, 32], 1)
@onChange(ParallaxMappingPlugin.prototype._updateExtension)
@serialize() stepCount = 12

@uiSlider('Binary search steps', [1, 8], 1)
@onChange(ParallaxMappingPlugin.prototype._updateExtension)
@serialize() binaryStepCount = 3

@onChange(ParallaxMappingPlugin.prototype._updateExtension)
@uiToggle('Debug Normals') debugNormals = false
@onChange(ParallaxMappingPlugin.prototype._updateExtension)
@uiToggle('Debug Hit Height') debugHitHeight = false

private _defines: any = {
['PARALLAX_NORMAL_MAP_QUALITY']: 0,
}

constructor(enabled = true) {
super()
this.enabled = enabled
this._updateExtension = this._updateExtension.bind(this)
}

private _updateExtension() {
this._bumpMapExtension?.setDirty?.()
this._viewer?.setDirty()
}
private _bumpMapExtension: MaterialExtension = {
shaderExtender: (shader, material, _renderer) => {
if (!material.bumpMap || this.isDisabled()) return

shader.fragmentShader = shader.fragmentShader.replace('#include <normal_fragment_begin>', '')
shader.fragmentShader = shader.fragmentShader.replace('#include <normal_fragment_maps>', '')

shader.fragmentShader = shader.fragmentShader.replace('#include <map_fragment>',
'#include <normal_fragment_begin>\n#include <normal_fragment_maps>\n#include <map_fragment>')

for (const s of ['map_fragment', 'alphamap_fragment', 'roughnessmap_fragment', 'metalnessmap_fragment', 'emissivemap_fragment', 'transmission_fragment']) {
shader.fragmentShader = shaderReplaceString(shader.fragmentShader, `#include <${s}>`,
(ShaderChunk as any)[s].replace(/\bv\w+Uv\b/g, 'parallaxUv.xy', {replaceAll: true})
)
}

if (this.debugNormals || this.debugHitHeight)
shader.fragmentShader = shaderReplaceString(shader.fragmentShader,
// .replace('texture2D( map, parallaxUv.xy )', 'texture2D( map, parallaxUv.xy )')
'texture2D( map, parallaxUv.xy )',
this.debugNormals ? 'vec4(normal, 1.); normal = geometryNormal' : 'vec4(parallaxUv.z,0., 0., 1.)')


shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#include <normal_fragment_maps>',
shaderReplaceString(
shaderReplaceString(
ShaderChunk.normal_fragment_maps,
'#elif defined( USE_NORMALMAP_TANGENTSPACE )', '#elif defined( USE_NORMALMAP_TANGENTSPACE ) && !defined( USE_BUMPMAP )'),
'normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );',
// 'diffuseColor.rgb = vec3(0, dHdxy_fwd());'
// 'diffuseColor.rgb = CalculateNormal(vUv).rgb;'
'vec3 parallaxUv = reliefParallaxPerturbNormal(faceDirection, normal);'
)
)
},
parsFragmentSnippet: ()=> {
return this.isDisabled() ? '' : (ParallaxMappingPluginReliefShader + '\n')
.replaceAll('PARALLAX_MAP_STEPS', this.stepCount.toString()) // replacing here to unroll for loop.
.replaceAll('PARALLAX_MAP_B_STEPS', this.binaryStepCount.toString())
},
isCompatible: (material: PhysicalMaterial) => {
return material.isPhysicalMaterial
},
computeCacheKey: material => {
return '' + !this.isDisabled() + material.bumpMap?.uuid + this.debugNormals + this.debugHitHeight + this.stepCount.toString() + this.binaryStepCount.toString()
},
onObjectRender: (_object, material, _renderer) => {
if (this.isDisabled()) return // todo: use extraDefines
updateMaterialDefines({
...this._defines,
}, material)
},
} as MaterialExtension

onAdded(viewer: ThreeViewer) {
viewer.materialManager.registerMaterialExtension(this._bumpMapExtension)
return super.onAdded(viewer)
}

onRemove(viewer: ThreeViewer) {
viewer.materialManager.unregisterMaterialExtension(this._bumpMapExtension)
return super.onRemove(viewer)
}

}

+ 157
- 0
src/plugins/material/shaders/ParallaxMappingPlugin.relief.glsl View File

@@ -0,0 +1,157 @@

#ifdef USE_BUMPMAP

mat3 mat3_inverse( mat3 A )
{
mat3 M_t = mat3(
vec3( A[0][0], A[1][0], A[2][0] ),
vec3( A[0][1], A[1][1], A[2][1] ),
vec3( A[0][2], A[1][2], A[2][2] ) );
float det = dot( cross( M_t[0], M_t[1] ), M_t[2] );
mat3 adjugate = mat3( cross( M_t[1], M_t[2] ),
cross( M_t[2], M_t[0] ),
cross( M_t[0], M_t[1] ) );
return adjugate / det;
}


float CalculateHeight( in vec2 texCoords )
{
float height = texture2D( bumpMap, texCoords ).x;
return clamp( height, 0.0, 1.0 );
}

const vec2 bumpMapSize = vec2(512, 512);
// Return normal in tangent space from normal map if available or bump map
vec3 CalculateNormal( in vec2 texCoords )
{
#if defined( TANGENTSPACE_NORMALMAP ) && 0 //todo: fix. not working properly.
vec3 mapN = texture2D( normalMap, texCoords ).xyz;
mapN.xy *= normalScale;
return normalize( mapN );
#else
vec2 texOffs = 1.0 / bumpMapSize;
#if PARALLAX_NORMAL_MAP_QUALITY > 0
float hx[9];
hx[0] = texture2D( bumpMap, texCoords.st + texOffs * vec2(-1.0, -1.0) ).r;
hx[1] = texture2D( bumpMap, texCoords.st + texOffs * vec2( 0.0, -1.0) ).r;
hx[2] = texture2D( bumpMap, texCoords.st + texOffs * vec2( 1.0, -1.0) ).r;
hx[3] = texture2D( bumpMap, texCoords.st + texOffs * vec2(-1.0, 0.0) ).r;
hx[4] = texture2D( bumpMap, texCoords.st ).r;
hx[5] = texture2D( bumpMap, texCoords.st + texOffs * vec2( 1.0, 0.0) ).r;
hx[6] = texture2D( bumpMap, texCoords.st + texOffs * vec2(-1.0, 1.0) ).r;
hx[7] = texture2D( bumpMap, texCoords.st + texOffs * vec2( 0.0, 1.0) ).r;
hx[8] = texture2D( bumpMap, texCoords.st + texOffs * vec2( 1.0, 1.0) ).r;
vec2 deltaH = vec2(hx[0]-hx[2] + 2.0*(hx[3]-hx[5]) + hx[6]-hx[8], hx[0]-hx[6] + 2.0*(hx[1]-hx[7]) + hx[2]-hx[8]);
#else
float h_xa = texture2D( bumpMap, texCoords.st + texOffs * vec2(-1.0, 0.0) ).r;
float h_xb = texture2D( bumpMap, texCoords.st + texOffs * vec2( 1.0, 0.0) ).r;
float h_ya = texture2D( bumpMap, texCoords.st + texOffs * vec2( 0.0, -1.0) ).r;
float h_yb = texture2D( bumpMap, texCoords.st + texOffs * vec2( 0.0, 1.0) ).r;
vec2 deltaH = vec2(h_xa-h_xb, h_ya-h_yb);
#endif
return normalize( vec3( deltaH / texOffs, 1.0 ) );
#endif
}

//https://github.com/Rabbid76/graphics-snippets/blob/master/html/technique/parallax_005_parallax_relief_mapping_derivative_tbn.html
//https://web.archive.org/web/20190128023901/http://sunandblackcat.com/tipFullView.php?topicid=28
vec3 ReliefParallax( in float frontFace, in vec3 texDir3D, in vec2 texCoord )
{
float surf_sign = frontFace;
float back_face = step(0.0, -surf_sign);
vec2 texStep = surf_sign * texDir3D.xy / abs(texDir3D.z); // (z is negative) the direction vector points downwards in tangent-space
vec2 texC = texCoord.st + surf_sign * texStep + back_face * texStep.xy;
float mapHeight = 1.0;
float bumpHeightStep = 1.0 / float(PARALLAX_MAP_STEPS);
float bestBumpHeight = mapHeight+bumpHeightStep;

#pragma unroll_loop_start
for ( int i = 0 ; i < PARALLAX_MAP_STEPS ; i ++ ) {

if ( mapHeight < bestBumpHeight )
{
bestBumpHeight -= bumpHeightStep;
mapHeight = back_face + surf_sign * CalculateHeight(texC.xy - bestBumpHeight * texStep.xy);
}

}
#pragma unroll_loop_end

bestBumpHeight += bumpHeightStep;

#pragma unroll_loop_start
for ( int i = 0; i < PARALLAX_MAP_B_STEPS ; i ++ ) {

bumpHeightStep *= 0.5;
bestBumpHeight -= bumpHeightStep;
mapHeight = back_face + surf_sign * CalculateHeight( texC.xy - bestBumpHeight * texStep.xy );
bestBumpHeight += ( bestBumpHeight < mapHeight ) ? bumpHeightStep : 0.0;

}
#pragma unroll_loop_end

bestBumpHeight -= bumpHeightStep * clamp( ( bestBumpHeight - mapHeight ) / bumpHeightStep, 0.0, 1.0 );
mapHeight = bestBumpHeight;
texC -= mapHeight * texStep;

return vec3( texC.xy, mapHeight );
}

vec3 reliefParallaxPerturbNormal(in float faceDirection, inout vec3 normal){
if(abs(bumpScale) < 0.001) return vec3(vBumpMapUv, 0.);

// #ifdef DOUBLE_SIDED
//
// normal = normal * faceDirection;
//
// #endif

float parallaxHeight;

vec2 texCoords = vBumpMapUv;
float face_sign = sign(dot(normal, vViewPosition));

// Followup: Normal Mapping Without Precomputed Tangents [http://www.thetenthplanet.de/archives/1180]
vec3 N = normalize(normal);
vec3 dp1 = dFdx(-vViewPosition);
vec3 dp2 = dFdy(-vViewPosition);
vec2 duv1 = dFdx(vBumpMapUv);
vec2 duv2 = dFdy(vBumpMapUv);
vec3 dp2perp = cross(dp2, N);
vec3 dp1perp = cross(N, dp1);
vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
float invmax = inversesqrt(max(dot(T, T), dot(B, B)));
mat3 tbnMat = mat3(T * invmax, B * invmax, N * bumpScale);

vec3 tangentPos = normalize(mat3_inverse(tbnMat) * -vViewPosition);

// vec2 parallaxUv = parallaxMapping(tangentPos, vBumpMapUv, parallaxHeight);
vec3 parallaxUv = ReliefParallax(face_sign, tangentPos, vBumpMapUv);

tbnMat[2] = face_sign * N / bumpScale;

normal = normalize(tbnMat * CalculateNormal(parallaxUv.xy).xyz);

//todo test this.
#ifdef FLIP_SIDED

normal = - normal;

#endif

// #ifdef DOUBLE_SIDED
//
// normal = normal * faceDirection;
//
// #endif

// normal = geometryNormal;

// todo: modify geometry.position (vViewPosition) for point, spot and area lights

return parallaxUv;
}

#endif // USE_BUMPMAP

+ 5
- 0
src/plugins/pipeline/SSAAPlugin.ts View File

@@ -43,6 +43,11 @@ export class SSAAPlugin extends AViewerPluginSync<SSAAPluginEventTypes> {

dependencies = [ProgressivePlugin]

constructor(rendersPerFrame = 1) {
super()
this.rendersPerFrame = rendersPerFrame
}

onAdded(viewer: ThreeViewer) {
super.onAdded(viewer)
viewer.addEventListener('preRender', this._preRender)

Loading…
Cancel
Save