mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
103.04k stars 35.41k forks source link

Update shadows when using morph targets? #1828

Closed patrickkidd closed 12 years ago

patrickkidd commented 12 years ago

Is it possible to update the shadows on a mesh when animating with morph targets? When you rotate a textured object the shadows rotate with it...

Thanks!

alteredq commented 12 years ago

I'm not sure I understand the question. Shadows should just work with morphs:

http://mrdoob.github.com/three.js/examples/webgl_shadowmap.html http://mrdoob.github.com/three.js/examples/webgl_morphtargets_md2.html

patrickkidd commented 12 years ago

The shadows do update on other objects, but I am having trouble on the object that is morphed itself. Notice how the lighting changes with the following model is rotated (the individual cylinders also rotate inside it, and everything is contained in a single png-textured geometry using morph targets).

http://imgur.com/a/G88MS

The lighting is exactly the same in each frame when I take the spotlight away leaving just the ambient light:

http://imgur.com/a/EhSNq

I'm no expert with this stuff, so I'm sorry if I'm making a simple mistake here.

alteredq commented 12 years ago

Aha, I think this is not about shadows but about normals - what you need is to turn on morphNormals.

By default morphs just keep the normals from the first frame, so no matter which change happens in the animation, they will be lit as this first frame (this is cheaper and can look ok if morph poses don't differ a lot).

Check this example to see how morphNormals work:

http://mrdoob.github.com/three.js/examples/webgl_morphnormals.html

You'll need to do 2 things - turn on morphNormals flag in the material and compute normals for all animation frames like this:

geometry.computeMorphNormals();
patrickkidd commented 12 years ago

That did it, thanks! computeMorphNormals is slow, but that definitely answers the question.

patrickkidd commented 12 years ago

Another related question - I changed the "shading" property in the JSON materials to "Phong" to get some spectral highlights, and now found that it suddenly changes again when animating:

http://imgur.com/a/tYaah

The first one is when it's stationary, and the appearance suddenly changes to the second image when I start animating it. Am I doing something wrong?

alteredq commented 12 years ago

Hmmm, this could be normals issue yet again but a bit different.

The trouble with morph normals is that normals for animation frames are computed automatically while for static mesh you can have custom vertex normals loaded from the model file. Such user supplied normals can have more information than automatically computed ones, e.g. if smoothing groups were used in the modeling program.

Quick test if this is indeed the case could be to export model without any normals and just use automatic normals also for static model:

geometry.computeVertexNormals();
patrickkidd commented 12 years ago

Right again. It's also important to note that you have to call computeMorphNormals() before adding it to the scene or numMorphNormals will = 0 on geometryGroup forever.

It turns out that the normals calculated from the smoothing groups in 3DSMax look better than the three.js ones so I'll try to add support for them in my patched ThreeJSExporter.ms. This should also get around the prohibitively slow performance hit from computeMorphNormals as long as the JSON parsing isn't too slow as a result.

Am I right in assuming this or is there still some computation required if I have normals stored in JSON for each morphTarget? Maybe you can give me a tip on where to get my Loader to stick the morphNormals from MAX into the Geometry object? I see 'faceNormals' and 'vertexNormals' in geometry.morphNormals, not sure where they come from.

Thanks!

alteredq commented 12 years ago

Am I right in assuming this or is there still some computation required if I have normals stored in JSON for each morphTarget

I think not, it should be just like with vertex normals for static meshes, these would go directly from the JSON into appropriate data structures (the same place where now go computed vertex normals).

And for face normals, I guess the simplest would be just to ignore them for the moment (unless you would want to add yet another thing to JSON format or have separate method to compute just them).

These come in play just for flat shading and picking.

I see 'faceNormals' and 'vertexNormals' in geometry.morphNormals, not sure where they come from.

These are computed in geometry.computeMorphNormals() based on vertex positions for each morph pose.

patrickkidd commented 12 years ago

Getting normals and morph normals directly from MAX is preferred to take advantage of things like smoothing groups and also decrease the time it takes to set the object up because you don't have to compute the morph normals. I added morphTargets[n].normals to my MAX exporter (pull request coming, I promise :)), and wrote importMorphNormals as a replacement for computeMorphNormals. Can you do me a favor and have a look at my importMorphNormals? For some reason I'm seeing a change from the unmorphed shading and morphed shading again even when all normals are imported from the JSON. At first glance the values in json.morphTargets[n].normals looks OK...


    THREE.Geometry.prototype.importMorphNormals = function(json) {

        var i, il, n, nl, f, fl, x, y, z, vA, vB, vC, cb, ab,
            quads, morphTarget, morphNormals, faceNormals, vertexNormals, tmpVertexNormals

        quads = this.faces[0] instanceof THREE.Face4;

        for ( i = 0, il = this.morphTargets.length; i < il; i ++ ) {

            jsonNormals = json.morphTargets[ i ].normals;

            morphTarget = this.morphTargets[ i ];

            faceNormals = [];
            vertexNormals = [];
            morphNormals = this.morphNormals.push( {

                faceNormals: faceNormals,
                vertexNormals: vertexNormals

            } );

            // these are duplicated by reference in morphNormals[n].vertexNormals

            tmpVertexNormals = [];

            for( n = 0, nl = jsonNormals.length; n < nl; n += 3 ) {

                x = jsonNormals[ n ];
                y = jsonNormals[ n + 1 ];
                z = jsonNormals[ n + 2 ];

                tmpVertexNormals.push(new THREE.Vector3(x, y, z));
            }

            cb = new THREE.Vector3()
            ab = new THREE.Vector3();

            for ( f = 0, fl = this.faces.length; f < fl; f ++ ) {

                face = this.faces[ f ];

                // from Geometry.computeFaceNormals()

                vA = morphTarget.vertices[ face.a ];
                vB = morphTarget.vertices[ face.b ];
                vC = morphTarget.vertices[ face.c ];

                cb.sub( vC.position, vB.position );
                ab.sub( vA.position, vB.position );
                cb.crossSelf( ab );

                if ( !cb.isZero() ) {

                    cb.normalize();

                }

                faceNormals.push( cb.clone() );

                if( quads ) {

                    vertexNormals.push( {

                        a: tmpVertexNormals[ face.a ],
                        b: tmpVertexNormals[ face.b ],
                        c: tmpVertexNormals[ face.c ]

                    } )

                } else {

                    vertexNormals.push( {

                        a: tmpVertexNormals[ face.a ],
                        b: tmpVertexNormals[ face.b ],
                        c: tmpVertexNormals[ face.c ],
                        d: tmpVertexNormals[ face.d ]

                    } )

                }

            }

        }

    }
patrickkidd commented 12 years ago

@alteredq I forgot to mention you so you'd get alerted to my new comment. Is it better to create a new issue after this one goes a couple of weeks without activity or just mention you?

patrickkidd commented 12 years ago

Here is an example of what I am seeing:

http://imgur.com/a/Nml92

patrickkidd commented 12 years ago
    Loader = function(showStatus) {
        THREE.JSONLoader.call( this, showStatus );
    }

    Loader.prototype = new THREE.JSONLoader();
    Loader.prototype.constructor = Loader;
    Loader.prototype.supr = THREE.JSONLoader.prototype;

    var IMPORT_MORPHNORMALS = true

    Loader.prototype.createModel = function ( json, callback, texturePath ) {
        var Callback = function(geometry) {

            geometry.maxObjectName = json.metadata.name

            if ( json.morphTargets !== undefined ) {

                for(var i in json.morphTargets ) {
                    geometry.morphTargets[ i ].action = json.morphTargets[ i ].action; // pks: Animation Name
                    geometry.morphTargets[ i ].frame = json.morphTargets[ i ].keyframe; // pks: keyframe number
                }

                for(var i in geometry.materials)
                    geometry.materials[i].morphNormals = true

                if(IMPORT_MORPHNORMALS) {

                    if(json.metadata.normals > 0 && json.metadata.morphTargets > 0)
                        geometry.importMorphNormals(json)

                } else {

                    geometry.computeVertexNormals()
                    geometry.computeMorphNormals()

                }

            }

            if(callback)
                callback(geometry)

        }

        THREE.JSONLoader.prototype.createModel.call(this, json, Callback, texturePath)
    }
patrickkidd commented 12 years ago

here is the model JSON:

http://www.res3d.com/research/morphNormals/Roller-Screw.js

patrickkidd commented 12 years ago

Here is an runnable example:

http://www.res3d.com/research/morphNormals/roller-screw.zip

alteredq commented 12 years ago

That's a lot of code and many steps in the asset pipeline, it's hard to tell what's going on.

I would suggest to break it into smaller problems.

What you could try is to create static geometry from the morph vertices and morph normals for a particular animation frame (instead of usual vertices and normals arrays) and check if there shading looks as expected.

From the screenshots and runnable example it kinda looks like if morph states were completely unshaded (colors coming just from texture, not lighting).

patrickkidd commented 12 years ago

@alteredq I've trimmed it even more. MorphAnimMesh.js basically just progresses currentKeyframe like THREE.MorphAnimMesh, and RiggedPerspectiveCamera just controls the camera so you can ignore those two files. Here is an updated version:

http://www.res3d.com/research/morphNormals/roller-screw2.zip

Maybe the normals are flipped or something?

patrickkidd commented 12 years ago

@alteredq I tried making a new mesh for a single morph target but am getting the following error in chrome:

(10) GL_INVALID_OPERATION : glDrawXXX: attempt to access out of range vertices
WebGL: too many errors, no more errors will be reported to the console for this context. 

The following code block is how I'm constructing the geometry, assuming "this" is my main mesh from JSONLoader.js, and "morphTarget.tmpVertexNormals" is the vertex normals from the JSON model for that morph target. Am I leaving anything out?


            var morphTarget = this.geometry.morphTargets[frame]

            var geometry = new THREE.Geometry

            geometry.vertices = morphTarget.vertices

            // clone faces
            var normalIndex = 0

            var f, fl, face, faces = this.geometry.faces
            for(f = 0, fl = faces.length; f < fl; f++) {

                face = geometry.faces[ f ] = faces[ f ].clone()

                for(var vn=0; vn < 3; vn++) {

                    face.vertexNormals[ 0 ] = morphTarget.tmpVertexNormals[ face.a ]
                    face.vertexNormals[ 1 ] = morphTarget.tmpVertexNormals[ face.b ]
                    face.vertexNormals[ 2 ] = morphTarget.tmpVertexNormals[ face.c ]

                }

            }

            geometry.computeFaceNormals()

            var mesh = new THREE.Mesh(geometry, this.material);

            var m, ml
            for(m = 0, ml = this.geometry.materials.length; m < ml; m++)
                geometry.materials[m] = this.geometry.materials[m]

            mesh.scale = this.scale.clone()
patrickkidd commented 12 years ago

@alteredq @mrdoob : SOLVED.

I feel like I kicked up an issue storm with this one. Thanks for humoring me. It turned out that my max exporter was exporting the wrong values for the normals after all. I had to really dig into the values for a few days though.

Oh well, I had to walk the path of learning how the face and vertex normals work and how they are stored in morphNormals.

This is what it's supposed to look like:

http://www.res3d.com/research/morphNormals/roller-screw3.zip

mrdoob commented 12 years ago

Yay! :)

patrickkidd commented 12 years ago

Yeah, it it looks amazing with smoothing groups from max.

So are you guys interested in a pull request for the patched MAX exporter and accompanying importer code to support morph normals? If so I can issue a pull request with this stuff in it. I don't know if this is too far off your intended feature list or not. Also I use a code style that's less spacious than @alteredq but it works.

The max exporter has lots of new features now. The only ones I can remember right now are:

On May 15, 2012, at 3:59 PM, Mr.doob wrote:

Yay! :)


Reply to this email directly or view it on GitHub: https://github.com/mrdoob/three.js/issues/1828#issuecomment-5730762

mrdoob commented 12 years ago

I don't know about a pull request. But it you could share the exporter so we can take a look that'd be great :)

patrickkidd commented 12 years ago

OK, will do tomorrow.

On May 15, 2012, at 4:34 PM, Mr.doob wrote:

I don't know about a pull request. But it you could share the exporter so we can take a look that'd be great :)


Reply to this email directly or view it on GitHub: https://github.com/mrdoob/three.js/issues/1828#issuecomment-5731202

alteredq commented 12 years ago

Yay indeed ;)

I don't know about a pull request. But it you could share the exporter so we can take a look that'd be great :)

Seconded. It would be good to have it somewhere so that other Max users could use it / build upon it.

Or maybe we could eventually implement morphNormals also to other exporters.

This is what it's supposed to look like:

http://www.res3d.com/research/morphNormals/roller-screw3.zip

Looks much better, though it seems like some faces are inverted (probably because vertices are ordered differently).

I remember having such issues with Max. I think this comes from some modifiers.

It's not normally visible in Max as everything there is by default double sided, but there is a tool somewhere in Max menus to visualize these incorrect faces so you can fix them (I think it highlights them in yellow or green).

mrdoob commented 12 years ago

If I remember correctly it was something like "Xform reset" in... utils?

alteredq commented 12 years ago

Good keywords ;)

Here is what I managed to dig out of my Gmail outbox, instructions I sent for how to fix these:

  1. find out problematic objects in Max
    1. select all objects
    2. set View -> xView to show "Face Orientation"
      1. select all object that are fully/mostly green
    3. in Utilities apply Reset XForm to selected objects (this will show messed normals also in Max, otherwise they are hidden)
  2. fix problematic objects
    1. apply Normals => flipNormals modifier to selected object
mrdoob commented 12 years ago

Ah, the memories... It's been 10 years now since I had to do that process. :D

patrickkidd commented 12 years ago

OK guys, I pushed it to my fork. I'm an old CVS/svn guy so I'm new to this github craziness. The new exporter is here:

https://github.com/patrickkidd/three.js/tree/master/utils/exporters/max

This new version has lots of subtle tweaks that help modelers working with legitimate commercial0sized models like the max progress bar, cancel button, better I/O buffer for writing to network drives, multiple named output files, etc. We have a vested interest in getting three.js to work well with max so I have a bunch more features (materials, lights, object animation splines, animations w/ pos/scale/rot, etc) planned for this thing.

Check out the README.txt for changes, etc. There is also Loader.js that is modified to read the morph normals.

Thanks for all your help up to this point.