Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGLRenderer: Tight morph target packing. #27768

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Expand Up @@ -182,6 +182,7 @@
"webgl_modifier_tessellation",
"webgl_morphtargets",
"webgl_morphtargets_face",
"webgl_morphtargets_faces",
"webgl_morphtargets_horse",
"webgl_morphtargets_sphere",
"webgl_morphtargets_webcam",
Expand Down
Binary file added examples/screenshots/webgl_morphtargets_faces.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions examples/webgl_morphtargets_faces.html
@@ -0,0 +1,151 @@

<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - morph targets - faces</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="main.css">
<style>
body {
background-color: #666666;
}
</style>
</head>
<body>

<div id="info">
<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - morph targets - faces<br/>
model by <a href="https://www.bannaflak.com/face-cap" target="_blank" rel="noopener">Face Cap</a>
</div>

<script type="importmap">
{
"imports": {
"three": "../build/three.module.js",
"three/addons/": "./jsm/"
}
}
</script>

<script type="module">

import * as THREE from 'three';

import Stats from 'three/addons/libs/stats.module.js';

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';

import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';

init();

function init() {

let mixer, mesh, head, duration;
const clock = new THREE.Clock( true );

const container = document.createElement( 'div' );
document.body.appendChild( container );

const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 300 );
camera.position.set( 0, 0, 0 );

const scene = new THREE.Scene();

const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.toneMapping = THREE.ACESFilmicToneMapping;

container.appendChild( renderer.domElement );

const ktx2Loader = new KTX2Loader()
.setTranscoderPath( 'jsm/libs/basis/' )
.detectSupport( renderer );

new GLTFLoader()
.setKTX2Loader( ktx2Loader )
.setMeshoptDecoder( MeshoptDecoder )
.load( 'models/gltf/facecap.glb', ( gltf ) => {

const orig = gltf.scene.children[ 0 ];

mixer = new THREE.AnimationMixer( orig );

mixer.clipAction( gltf.animations[ 0 ] ).play();

duration = gltf.animations[ 0 ].duration;

// GUI

head = orig.getObjectByName( 'mesh_2' );

mesh = new THREE.InstancedMesh( head.geometry, head.material, 32 );

scene.add( mesh );

for ( let i = 0; i < 32; i ++ ) {

head.position.set( i % 8 - 4, Math.floor( i / 8 ) - 1, - 10 ).multiplyScalar( 25 );

head.rotation.x = Math.PI / 2;

head.updateMatrix();

mesh.setMatrixAt( i, head.matrix );

}

} );

const environment = new RoomEnvironment( renderer );
const pmremGenerator = new THREE.PMREMGenerator( renderer );

scene.background = new THREE.Color( 0x666666 );
scene.environment = pmremGenerator.fromScene( environment ).texture;


const stats = new Stats();
container.appendChild( stats.dom );

renderer.setAnimationLoop( () => {

const time = clock.getElapsedTime();

if ( mixer ) {

for ( let i = 0; i < 32; i ++ ) {

mixer.setTime( time + i * duration / 32 );

mesh.setMorphAt( i, head );

}

mesh.morphTexture.needsUpdate = true;

}

renderer.render( scene, camera );

stats.update();

} );

window.addEventListener( 'resize', () => {

camera.aspect = window.innerWidth / window.innerHeight;

camera.updateProjectionMatrix();

renderer.setSize( window.innerWidth, window.innerHeight );

} );

}
</script>
</body>
</html>
26 changes: 24 additions & 2 deletions src/renderers/shaders/ShaderChunk/morphtarget_pars_vertex.glsl.js
Expand Up @@ -20,12 +20,34 @@ export default /* glsl */`

vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {

int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;
int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + 3 * offset;
Copy link
Collaborator

@Mugen87 Mugen87 Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to multiply the offset with 3 now?

Copy link
Contributor Author

@wizgrav wizgrav Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep in line with what we previously had on the shader chunks that call getMorph. Now the position is at offset 0, normals at offset 3 and colors at offset 6. Alternatively we can modify the other shader chunks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid any misunderstanding, you want me to change the other shader chunks to eliminate the multiplication with 3? At some point we will probably have to revisit that part anyway if/when we add additional morphing attributes like uv. When that time comes more extensive changes on how we handle morphing will be needed because now it's a bit fixed like if there's a color morph but not normals the latter will be empty space in the texture. This is how it used to be even before this PR but we can make it more flexible when more attributes are introduced

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the current version of this PR 👍 .


int y = texelIndex / morphTargetsTextureSize.x;
int x = texelIndex - y * morphTargetsTextureSize.x;

ivec3 morphUV = ivec3( x, y, morphTargetIndex );
return texelFetch( morphTargetsTexture, morphUV, 0 );
Copy link
Collaborator

@Mugen87 Mugen87 Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are multiple fetches really faster than a single one? I'm afraid this needs to be tested one more than one device. Especially mobile devices could show a performance regression here.

Copy link
Contributor Author

@wizgrav wizgrav Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it is currently with rgba format it will always fetch 4 floats so fetching 3 instead should always be faster especially on mobiles where bandwidth is precious. I'll replace the horse example with the faces one and do some more testing but I only have one android phone with an older snapdragon and the quest 3 which all show similar improvements

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could also add a slider to webgl_instancing_morph that controls the number of instances.

Copy link
Contributor Author

@wizgrav wizgrav Feb 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have the faces instead because they are using more influences and also morph the normals so its a more complete case than the horses.


vec4 ret = vec4(0.);

ret.x = texelFetch( morphTargetsTexture, morphUV, 0 ).r;

morphUV.x++;

ret.y = texelFetch( morphTargetsTexture, morphUV, 0 ).r;

morphUV.x++;

ret.z = texelFetch( morphTargetsTexture, morphUV, 0 ).r;

#if MORPHTARGETS_TEXTURE_STRIDE == 10

morphUV.x++;

ret.a = offset == 2 ? texelFetch( morphTargetsTexture, morphUV, 0 ).r : 0.;

#endif

return ret;

}

Expand Down
37 changes: 18 additions & 19 deletions src/renderers/webgl/WebGLMorphtargets.js
@@ -1,4 +1,4 @@
import { FloatType } from '../../constants.js';
import { FloatType, RedFormat } from '../../constants.js';
import { DataArrayTexture } from '../../textures/DataArrayTexture.js';
import { Vector4 } from '../../math/Vector4.js';
import { Vector2 } from '../../math/Vector2.js';
Expand Down Expand Up @@ -58,41 +58,42 @@ function WebGLMorphtargets( gl, capabilities, textures ) {

let vertexDataCount = 0;

if ( hasMorphPosition === true ) vertexDataCount = 1;
if ( hasMorphNormals === true ) vertexDataCount = 2;
if ( hasMorphColors === true ) vertexDataCount = 3;
if ( hasMorphPosition === true ) vertexDataCount = 3;
if ( hasMorphNormals === true ) vertexDataCount = 6;
if ( hasMorphColors === true ) vertexDataCount = 10;

let width = geometry.attributes.position.count * vertexDataCount;
let height = 1;

if ( width > capabilities.maxTextureSize ) {

height = Math.ceil( width / capabilities.maxTextureSize );
// Align on stride to simplify the texel fetching in the shader
const strideWidth = Math.ceil( width / vertexDataCount ) * vertexDataCount;
height = Math.ceil( strideWidth / capabilities.maxTextureSize );
width = capabilities.maxTextureSize;

}

const buffer = new Float32Array( width * height * 4 * morphTargetsCount );
const buffer = new Float32Array( width * height * morphTargetsCount );

const texture = new DataArrayTexture( buffer, width, height, morphTargetsCount );
texture.type = FloatType;
texture.format = RedFormat;
texture.needsUpdate = true;

// fill buffer

const vertexDataStride = vertexDataCount * 4;

for ( let i = 0; i < morphTargetsCount; i ++ ) {

const morphTarget = morphTargets[ i ];
const morphNormal = morphNormals[ i ];
const morphColor = morphColors[ i ];

const offset = width * height * 4 * i;
const offset = width * height * i;

for ( let j = 0; j < morphTarget.count; j ++ ) {

const stride = j * vertexDataStride;
const stride = j * vertexDataCount;

if ( hasMorphPosition === true ) {

Expand All @@ -101,29 +102,27 @@ function WebGLMorphtargets( gl, capabilities, textures ) {
buffer[ offset + stride + 0 ] = morph.x;
buffer[ offset + stride + 1 ] = morph.y;
buffer[ offset + stride + 2 ] = morph.z;
buffer[ offset + stride + 3 ] = 0;

}

if ( hasMorphNormals === true ) {

morph.fromBufferAttribute( morphNormal, j );

buffer[ offset + stride + 4 ] = morph.x;
buffer[ offset + stride + 5 ] = morph.y;
buffer[ offset + stride + 6 ] = morph.z;
buffer[ offset + stride + 7 ] = 0;
buffer[ offset + stride + 3 ] = morph.x;
buffer[ offset + stride + 4 ] = morph.y;
buffer[ offset + stride + 5 ] = morph.z;

}

if ( hasMorphColors === true ) {

morph.fromBufferAttribute( morphColor, j );

buffer[ offset + stride + 8 ] = morph.x;
buffer[ offset + stride + 9 ] = morph.y;
buffer[ offset + stride + 10 ] = morph.z;
buffer[ offset + stride + 11 ] = ( morphColor.itemSize === 4 ) ? morph.w : 1;
buffer[ offset + stride + 6 ] = morph.x;
buffer[ offset + stride + 7 ] = morph.y;
buffer[ offset + stride + 8 ] = morph.z;
buffer[ offset + stride + 9 ] = ( morphColor.itemSize === 4 ) ? morph.w : 1;

}

Expand Down
6 changes: 3 additions & 3 deletions src/renderers/webgl/WebGLPrograms.js
Expand Up @@ -80,9 +80,9 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities

let morphTextureStride = 0;

if ( geometry.morphAttributes.position !== undefined ) morphTextureStride = 1;
if ( geometry.morphAttributes.normal !== undefined ) morphTextureStride = 2;
if ( geometry.morphAttributes.color !== undefined ) morphTextureStride = 3;
if ( geometry.morphAttributes.position !== undefined ) morphTextureStride = 3;
if ( geometry.morphAttributes.normal !== undefined ) morphTextureStride = 6;
if ( geometry.morphAttributes.color !== undefined ) morphTextureStride = 10;

//

Expand Down