From 742dc30f9e22c0770064c0ecb5ef0c701bb9b551 Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Tue, 8 May 2007 01:50:07 +0000 Subject: [PATCH] Basic support for skinning with GLSL shaders. Still need to work out how to handle different lighting arrangements and complex materials (e.g., the metal effect for the Iron Plate). git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@229 ed5b42cb-e716-0410-a449-f6a68f950b19 --- rsrc/media/jme/skin.vert | 85 +++++++ .../com/threerings/jme/model/ModelMesh.java | 21 +- .../com/threerings/jme/model/ModelNode.java | 12 + .../threerings/jme/model/ModelSpatial.java | 19 +- .../com/threerings/jme/model/SkinMesh.java | 119 ++++++++- .../com/threerings/jme/tools/ModelViewer.java | 240 +++++++++--------- src/java/com/threerings/jme/util/JmeUtil.java | 25 +- .../com/threerings/jme/util/ShaderCache.java | 184 ++++++++++++++ 8 files changed, 549 insertions(+), 156 deletions(-) create mode 100644 rsrc/media/jme/skin.vert create mode 100644 src/java/com/threerings/jme/util/ShaderCache.java diff --git a/rsrc/media/jme/skin.vert b/rsrc/media/jme/skin.vert new file mode 100644 index 00000000..2a5b7aab --- /dev/null +++ b/rsrc/media/jme/skin.vert @@ -0,0 +1,85 @@ +// +// $Id: ImageCache.java 158 2007-02-24 00:38:17Z mdb $ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +#version 110 + +#if BONES_PER_VERTEX == 4 + #define ATTRIB_TYPE vec4 +#elif BONES_PER_VERTEX == 3 + #define ATTRIB_TYPE vec3 +#elif BONES_PER_VERTEX == 2 + #define ATTRIB_TYPE vec2 +#else + #define ATTRIB_TYPE float +#endif + +/** The bone transforms. */ +uniform mat4 boneTransforms[MAX_BONE_COUNT]; + +/** The bone indices. */ +attribute ATTRIB_TYPE boneIndices; + +/** The bone weights. */ +attribute ATTRIB_TYPE boneWeights; + +/** + * Vertex shader for skinned meshes. + */ +void main () +{ + vec4 normal4 = vec4(gl_Normal, 0.0); + + // add up the vertex as transformed by each bone and scaled by each weight + #if BONES_PER_VERTEX == 1 + vec4 skinVertex = boneTransforms[int(boneIndices)] * gl_Vertex; + vec4 skinNormal = boneTransforms[int(boneIndices)] * normal4; + #else + vec4 skinVertex = boneTransforms[int(boneIndices[0])] * gl_Vertex * boneWeights[0]; + vec4 skinNormal = boneTransforms[int(boneIndices[0])] * normal4 * boneWeights[0]; + for (int ii = 1; ii < BONES_PER_VERTEX; ii++) { + skinVertex += boneTransforms[int(boneIndices[ii])] * gl_Vertex * boneWeights[ii]; + skinNormal += boneTransforms[int(boneIndices[ii])] * normal4 * boneWeights[ii]; + } + #endif + + // copy the texture coordinates from attribute to varying + gl_TexCoord[0] = gl_MultiTexCoord0; + gl_TexCoord[1] = gl_MultiTexCoord1; + + // apply the standard transformation + gl_Position = gl_ModelViewProjectionMatrix * skinVertex; + + // eye space 'z' is the standard fog coordinate + gl_FogFragCoord = -dot(gl_ModelViewMatrixTranspose[2], skinVertex); + + // transform the normal into eye space + vec3 eyeNormal = normalize(vec3(gl_ModelViewMatrixInverseTranspose * skinNormal)); + + // set gl_FrontColor based on normal and light parameters + gl_FrontColor.rgb = + gl_FrontLightProduct[0].ambient.rgb + + gl_FrontLightProduct[0].diffuse.rgb * + max(dot(eyeNormal, gl_LightSource[0].position.xyz), 0.0) + + gl_FrontLightProduct[1].ambient.rgb + + gl_FrontLightProduct[1].diffuse.rgb * + max(dot(eyeNormal, gl_LightSource[1].position.xyz), 0.0); + gl_FrontColor.a = gl_FrontMaterial.diffuse.a; +} diff --git a/src/java/com/threerings/jme/model/ModelMesh.java b/src/java/com/threerings/jme/model/ModelMesh.java index 3f06ebb2..ec15625e 100644 --- a/src/java/com/threerings/jme/model/ModelMesh.java +++ b/src/java/com/threerings/jme/model/ModelMesh.java @@ -67,6 +67,7 @@ import com.samskivert.util.PropertiesUtil; import com.samskivert.util.StringUtil; import com.threerings.jme.Log; +import com.threerings.jme.util.ShaderCache; /** * A {@link TriMesh} with a serialization mechanism tailored to stored models. @@ -441,6 +442,12 @@ public class ModelMesh extends TriMesh } } + // documentation inherited from interface ModelSpatial + public void configureShaders (ShaderCache scache) + { + // no-op + } + // documentation inherited from interface ModelSpatial public void storeMeshFrame (int frameId, boolean blend) { @@ -603,20 +610,6 @@ public class ModelMesh extends TriMesh return astate; } - /** - * Concatenates the two provided buffers. - */ - protected static FloatBuffer concatenate (FloatBuffer b1, FloatBuffer b2) - { - FloatBuffer nb = BufferUtils.createFloatBuffer(b1.capacity() + b2.capacity()); - b1.clear(); - nb.put(b1); - b2.clear(); - nb.put(b2); - nb.clear(); - return nb; - } - /** * Sorts the encoded triangle index/distance pairs in {@link #_tcodes} * using a two-pass (16 bit) radix sort (as described by diff --git a/src/java/com/threerings/jme/model/ModelNode.java b/src/java/com/threerings/jme/model/ModelNode.java index a6139eae..ce96da7f 100644 --- a/src/java/com/threerings/jme/model/ModelNode.java +++ b/src/java/com/threerings/jme/model/ModelNode.java @@ -42,6 +42,7 @@ import com.jme.util.export.OutputCapsule; import com.threerings.jme.Log; import com.threerings.jme.util.JmeUtil; +import com.threerings.jme.util.ShaderCache; /** * A {@link Node} with a serialization mechanism tailored to stored models. @@ -259,6 +260,17 @@ public class ModelNode extends Node } } + // documentation inherited from interface ModelSpatial + public void configureShaders (ShaderCache scache) + { + for (int ii = 0, nn = getQuantity(); ii < nn; ii++) { + Spatial child = getChild(ii); + if (child instanceof ModelSpatial) { + ((ModelSpatial)child).configureShaders(scache); + } + } + } + // documentation inherited from interface ModelSpatial public void storeMeshFrame (int frameId, boolean blend) { diff --git a/src/java/com/threerings/jme/model/ModelSpatial.java b/src/java/com/threerings/jme/model/ModelSpatial.java index cdaf6389..a56c1244 100644 --- a/src/java/com/threerings/jme/model/ModelSpatial.java +++ b/src/java/com/threerings/jme/model/ModelSpatial.java @@ -24,6 +24,8 @@ package com.threerings.jme.model; import com.jme.renderer.Renderer; import com.jme.scene.Spatial; +import com.threerings.jme.util.ShaderCache; + /** * Contains method common to both {@link ModelNode}s and {@link ModelMesh}es. */ @@ -34,7 +36,7 @@ public interface ModelSpatial * they include the current vertex positions. */ public void expandModelBounds (); - + /** * Recursively sets the reference transforms for any bones in the model. */ @@ -50,12 +52,17 @@ public interface ModelSpatial */ public void lockStaticMeshes ( Renderer renderer, boolean useVBOs, boolean useDisplayLists); - + /** * Recursively resolves texture references using the given provider. */ public void resolveTextures (TextureProvider tprov); - + + /** + * Recursively creates or reconfigures any shaders used by the model using the supplied cache. + */ + public void configureShaders (ShaderCache scache); + /** * Recursively requests that the current state of all skinned meshes be * stored as an animation frame on the next update. @@ -67,19 +74,19 @@ public interface ModelSpatial * {@link #setAnimationFrames} */ public void storeMeshFrame (int frameId, boolean blend); - + /** * Recursively switches all skinned meshes to a stored animation frame for * the next update. */ public void setMeshFrame (int frameId); - + /** * Recursively blends all skinned meshes between two stored animation * frames for the next update. */ public void blendMeshFrames (int frameId1, int frameId2, float alpha); - + /** * Creates or populates and returns a clone of this object using the given * clone properties. diff --git a/src/java/com/threerings/jme/model/SkinMesh.java b/src/java/com/threerings/jme/model/SkinMesh.java index 6dc6735b..9d5c0b6f 100644 --- a/src/java/com/threerings/jme/model/SkinMesh.java +++ b/src/java/com/threerings/jme/model/SkinMesh.java @@ -33,6 +33,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import org.lwjgl.opengl.GLContext; + import com.jme.bounding.BoundingVolume; import com.jme.math.Matrix4f; import com.jme.math.Vector3f; @@ -42,7 +44,10 @@ import com.jme.scene.TriMesh; import com.jme.scene.VBOInfo; import com.jme.scene.batch.SharedBatch; import com.jme.scene.batch.TriangleBatch; +import com.jme.scene.state.GLSLShaderObjectsState; +import com.jme.scene.state.RenderState; import com.jme.system.DisplaySystem; +import com.jme.util.ShaderAttribute; import com.jme.util.export.JMEExporter; import com.jme.util.export.JMEImporter; import com.jme.util.export.InputCapsule; @@ -52,15 +57,23 @@ import com.jme.util.geom.BufferUtils; import com.samskivert.util.ArrayUtil; import com.samskivert.util.HashIntMap; +import com.samskivert.util.ListUtil; import com.threerings.jme.Log; import com.threerings.jme.util.JmeUtil; +import com.threerings.jme.util.ShaderCache; /** * A triangle mesh that deforms according to a bone hierarchy. */ public class SkinMesh extends ModelMesh { + /** The maximum number of bone matrices that we can use for hardware skinning. */ + public static final int MAX_SHADER_BONE_COUNT = 32; + + /** The maximum number of bones influencing a single vertex for hardware skinning. */ + public static final int MAX_SHADER_BONES_PER_VERTEX = 4; + /** Represents the vertex weights of a group of vertices influenced by the * same set of bones. */ public static class WeightGroup @@ -239,12 +252,26 @@ public class SkinMesh extends ModelMesh } else { mstore = (SkinMesh)store; } - properties.removeProperty("vertices"); - properties.removeProperty("normals"); + GLSLShaderObjectsState sstate = (GLSLShaderObjectsState)getRenderState( + RenderState.RS_GLSL_SHADER_OBJECTS); + if (sstate == null) { + // vertices and normals must be cloned if not using a shader + properties.removeProperty("vertices"); + properties.removeProperty("normals"); + } properties.removeProperty("displaylistid"); super.putClone(mstore, properties); - properties.addProperty("vertices"); - properties.addProperty("normals"); + if (sstate == null) { + properties.addProperty("vertices"); + properties.addProperty("normals"); + } else { + // for the shader, we must create a separate instance with different uniforms + GLSLShaderObjectsState msstate = + DisplaySystem.getDisplaySystem().getRenderer().createGLSLShaderObjectsState(); + msstate.setProgramID(sstate.getProgramID()); + msstate.attribs = sstate.attribs; + mstore.setRenderState(msstate); + } properties.addProperty("displaylistid"); mstore._frames = _frames; mstore._useDisplayLists = _useDisplayLists; @@ -261,8 +288,8 @@ public class SkinMesh extends ModelMesh } mstore._ovbuf = _ovbuf; mstore._onbuf = _onbuf; - mstore._vbuf = new float[_vbuf.length]; - mstore._nbuf = new float[_nbuf.length]; + mstore._vbuf = (sstate == null) ? _vbuf : new float[_vbuf.length]; + mstore._nbuf = (sstate == null) ? _nbuf : new float[_nbuf.length]; return mstore; } @@ -323,10 +350,48 @@ public class SkinMesh extends ModelMesh vboinfo.setVBOTextureEnabled(true); vboinfo.setVBOIndexEnabled(!_translucent); setVBOInfo(vboinfo); + + // use VBOs for shader attributes + GLSLShaderObjectsState sstate = (GLSLShaderObjectsState)getRenderState( + RenderState.RS_GLSL_SHADER_OBJECTS); + if (sstate != null) { + for (ShaderAttribute attrib : sstate.attribs.values()) { + attrib.useVBO = true; + } + } } _useDisplayLists = useDisplayLists && !_translucent; } + @Override // documentation inherited + public void configureShaders (ShaderCache scache) + { + if (_disableShaders || !GLContext.getCapabilities().GL_ARB_vertex_shader || + _bones.length > MAX_SHADER_BONE_COUNT) { + return; + } + int bonesPerVertex = 0; + for (WeightGroup group : _weightGroups) { + bonesPerVertex = Math.max(group.bones.length, bonesPerVertex); + } + if (bonesPerVertex > MAX_SHADER_BONES_PER_VERTEX) { + return; + } + GLSLShaderObjectsState sstate = (GLSLShaderObjectsState)getRenderState( + RenderState.RS_GLSL_SHADER_OBJECTS); + if (sstate == null) { + sstate = DisplaySystem.getDisplaySystem().getRenderer().createGLSLShaderObjectsState(); + setShaderAttributes(sstate, bonesPerVertex); + setRenderState(sstate); + } + if (!scache.configureState(sstate, "media/jme/skin.vert", null, + "MAX_BONE_COUNT " + MAX_SHADER_BONE_COUNT, "BONES_PER_VERTEX " + bonesPerVertex)) { + clearRenderState(RenderState.RS_GLSL_SHADER_OBJECTS); + _disableShaders = true; + return; + } + } + @Override // documentation inherited public void storeMeshFrame (int frameId, boolean blend) { @@ -375,6 +440,16 @@ public class SkinMesh extends ModelMesh bone.transform.multLocal(bone.invRefTransform); } + // if we're using shaders, initialize the uniform variables with the bone transforms + GLSLShaderObjectsState sstate = (GLSLShaderObjectsState)getRenderState( + RenderState.RS_GLSL_SHADER_OBJECTS); + if (sstate != null) { + for (int ii = 0; ii < _bones.length; ii++) { + sstate.setUniform("boneTransforms[" + ii + "]", _bones[ii].transform, true); + } + return; + } + // deform the mesh according to the positions of the bones (this code // is ugly as sin because it's optimized at a low level) Bone[] bones; @@ -481,6 +556,35 @@ public class SkinMesh extends ModelMesh _nbuf = new float[_onbuf.length]; } + /** + * Initializes the skin shader attributes (bone indices and weights) in the supplied state. + */ + protected void setShaderAttributes (GLSLShaderObjectsState sstate, int bonesPerVertex) + { + int size = getBatch(0).getVertexCount() * bonesPerVertex; + ByteBuffer bibuf = BufferUtils.createByteBuffer(size); + FloatBuffer bwbuf = BufferUtils.createFloatBuffer(size); + + for (WeightGroup group : _weightGroups) { + byte[] indices = new byte[bonesPerVertex]; + for (int ii = 0; ii < indices.length; ii++) { + indices[ii] = (byte)((ii < group.bones.length) ? + ListUtil.indexOf(_bones, group.bones[ii]) : 0); + } + for (int ii = 0, widx = 0; ii < group.vertexCount; ii++) { + bibuf.put(indices); + for (int jj = 0; jj < bonesPerVertex; jj++) { + bwbuf.put((jj < group.bones.length) ? group.weights[widx++] : 0f); + } + } + } + bibuf.rewind(); + bwbuf.rewind(); + + sstate.setAttributePointer("boneIndices", bonesPerVertex, false, false, 0, bibuf); + sstate.setAttributePointer("boneWeights", bonesPerVertex, false, 0, bwbuf); + } + /** A stored frame used for linear blending. */ protected static class BlendFrame { @@ -535,6 +639,9 @@ public class SkinMesh extends ModelMesh /** Whether or not the stored frame id will be used for blending. */ protected boolean _storeBlend; + /** Set if we determine that our shaders don't compile to prevent us from trying again. */ + protected static boolean _disableShaders; + /** A dummy mesh that simply hold transformation values. */ protected static final TriMesh DUMMY_MESH = new TriMesh(); diff --git a/src/java/com/threerings/jme/tools/ModelViewer.java b/src/java/com/threerings/jme/tools/ModelViewer.java index 4632d5c9..8f884b82 100644 --- a/src/java/com/threerings/jme/tools/ModelViewer.java +++ b/src/java/com/threerings/jme/tools/ModelViewer.java @@ -114,6 +114,7 @@ import com.threerings.jme.Log; import com.threerings.jme.camera.CameraHandler; import com.threerings.jme.model.Model; import com.threerings.jme.model.TextureProvider; +import com.threerings.jme.util.ShaderCache; import com.threerings.jme.util.SpatialVisitor; /** @@ -142,7 +143,7 @@ public class ModelViewer extends JmeCanvasApp LoggingSystem.getLoggingSystem().setLevel(Level.WARNING); new ModelViewer(args.length > 0 ? args[0] : null); } - + /** * Creates and initializes an instance of the model viewer application. * @@ -153,17 +154,18 @@ public class ModelViewer extends JmeCanvasApp { super(1024, 768); _rsrcmgr = new ResourceManager("rsrc"); + _scache = new ShaderCache(_rsrcmgr); _msg = new MessageManager("rsrc.i18n").getBundle("jme.viewer"); _path = path; - + JPopupMenu.setDefaultLightWeightPopupEnabled(false); - + _frame = new JFrame(_msg.get("m.title")); _frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - + JMenuBar menu = new JMenuBar(); _frame.setJMenuBar(menu); - + JMenu file = new JMenu(_msg.get("m.file_menu")); file.setMnemonic(KeyEvent.VK_F); menu.add(file); @@ -176,7 +178,7 @@ public class ModelViewer extends JmeCanvasApp load.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_L, KeyEvent.CTRL_MASK)); file.add(load); - + Action importAction = new AbstractAction(_msg.get("m.file_import")) { public void actionPerformed (ActionEvent e) { showImportDialog(); @@ -186,7 +188,7 @@ public class ModelViewer extends JmeCanvasApp importAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_I, KeyEvent.CTRL_MASK)); file.add(importAction); - + file.addSeparator(); Action quit = new AbstractAction(_msg.get("m.file_quit")) { public void actionPerformed (ActionEvent e) { @@ -197,11 +199,11 @@ public class ModelViewer extends JmeCanvasApp quit.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_MASK)); file.add(quit); - + JMenu view = new JMenu(_msg.get("m.view_menu")); view.setMnemonic(KeyEvent.VK_V); menu.add(view); - + Action wireframe = new AbstractAction(_msg.get("m.view_wireframe")) { public void actionPerformed (ActionEvent e) { _wfstate.setEnabled(!_wfstate.isEnabled()); @@ -212,26 +214,26 @@ public class ModelViewer extends JmeCanvasApp KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.CTRL_MASK)); view.add(new JCheckBoxMenuItem(wireframe)); view.addSeparator(); - + _pivots = new JCheckBoxMenuItem(_msg.get("m.view_pivots")); _pivots.setMnemonic(KeyEvent.VK_P); _pivots.setAccelerator( KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_MASK)); view.add(_pivots); - + _bounds = new JCheckBoxMenuItem(_msg.get("m.view_bounds")); _bounds.setMnemonic(KeyEvent.VK_B); _bounds.setAccelerator( KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_MASK)); view.add(_bounds); - + _normals = new JCheckBoxMenuItem(_msg.get("m.view_normals")); _normals.setMnemonic(KeyEvent.VK_N); _normals.setAccelerator( KeyStroke.getKeyStroke(KeyEvent.VK_N, KeyEvent.CTRL_MASK)); view.add(_normals); view.addSeparator(); - + Action campos = new AbstractAction(_msg.get("m.view_campos")) { public void actionPerformed (ActionEvent e) { _campos.setVisible(!_campos.isVisible()); @@ -242,10 +244,10 @@ public class ModelViewer extends JmeCanvasApp KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_MASK)); view.add(new JCheckBoxMenuItem(campos)); view.addSeparator(); - + _vmenu = new JMenu(_msg.get("m.model_variant")); view.add(_vmenu); - + JMenu amode = new JMenu(_msg.get("m.animation_mode")); final JRadioButtonMenuItem flipbook = new JRadioButtonMenuItem(_msg.get("m.mode_flipbook")), @@ -268,7 +270,7 @@ public class ModelViewer extends JmeCanvasApp } } }; - + mgroup.add(flipbook); mgroup.add(morph); mgroup.add(skin); @@ -278,7 +280,7 @@ public class ModelViewer extends JmeCanvasApp amode.add(flipbook); view.add(amode); view.addSeparator(); - + Action rlight = new AbstractAction(_msg.get("m.view_light")) { public void actionPerformed (ActionEvent e) { if (_rldialog == null) { @@ -292,7 +294,7 @@ public class ModelViewer extends JmeCanvasApp rlight.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_MASK)); view.add(new JMenuItem(rlight)); - + Action rcamera = new AbstractAction(_msg.get("m.view_recenter")) { public void actionPerformed (ActionEvent e) { ((OrbitCameraHandler)_camhand).recenter(); @@ -303,12 +305,12 @@ public class ModelViewer extends JmeCanvasApp rcamera.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_MASK)); view.add(new JMenuItem(rcamera)); - + _frame.getContentPane().add(getCanvas(), BorderLayout.CENTER); - + JPanel bpanel = new JPanel(new BorderLayout()); _frame.getContentPane().add(bpanel, BorderLayout.SOUTH); - + _animctrls = new JPanel(); _animctrls.setBorder(BorderFactory.createEtchedBorder()); bpanel.add(_animctrls, BorderLayout.NORTH); @@ -355,26 +357,26 @@ public class ModelViewer extends JmeCanvasApp } }); _animctrls.setVisible(false); - + JPanel spanel = new JPanel(new BorderLayout()); bpanel.add(spanel, BorderLayout.SOUTH); - + _status = new JLabel(" "); _status.setHorizontalAlignment(JLabel.LEFT); _status.setBorder(BorderFactory.createEtchedBorder()); spanel.add(_status, BorderLayout.CENTER); - + _campos = new JLabel(" "); _campos.setBorder(BorderFactory.createEtchedBorder()); _campos.setVisible(false); spanel.add(_campos, BorderLayout.EAST); - + _frame.pack(); _frame.setVisible(true); run(); } - + @Override // documentation inherited public boolean init () { @@ -386,7 +388,7 @@ public class ModelViewer extends JmeCanvasApp } return true; } - + @Override // documentation inherited protected void initDisplay () throws JmeException @@ -395,23 +397,23 @@ public class ModelViewer extends JmeCanvasApp _ctx.getRenderer().setBackgroundColor(ColorRGBA.gray); _ctx.getRenderer().getQueue().setTwoPassTransparency(false); } - + @Override // documentation inherited protected void initInput () { super.initInput(); - + _camhand.setTiltLimits(-FastMath.HALF_PI, FastMath.HALF_PI); _camhand.setZoomLimits(1f, 500f); _camhand.tiltCamera(-FastMath.PI * 7.0f / 16.0f); updateCameraPosition(); - + MouseOrbiter orbiter = new MouseOrbiter(); _canvas.addMouseListener(orbiter); _canvas.addMouseMotionListener(orbiter); _canvas.addMouseWheelListener(orbiter); } - + /** * Updates the camera position label. */ @@ -429,18 +431,18 @@ public class ModelViewer extends JmeCanvasApp " HP: " + CAMPOS_FORMAT.format(heading) + ", " + CAMPOS_FORMAT.format(pitch)); } - + @Override // documentation inherited protected CameraHandler createCameraHandler (Camera camera) { return new OrbitCameraHandler(camera); } - + @Override // documentation inherited protected void initRoot () { super.initRoot(); - + // set default states MaterialState mstate = _ctx.getRenderer().createMaterialState(); mstate.getDiffuse().set(ColorRGBA.white); @@ -450,7 +452,7 @@ public class ModelViewer extends JmeCanvasApp _wfstate = _ctx.getRenderer().createWireframeState()); _ctx.getGeometry().setNormalsMode(Spatial.NM_GL_NORMALIZE_PROVIDED); _wfstate.setEnabled(false); - + // create a grid on the XY plane to provide some reference Vector3f[] points = new Vector3f[GRID_SIZE*2 + GRID_SIZE*2]; float halfLength = (GRID_SIZE - 1) * GRID_SPACING / 2; @@ -466,7 +468,7 @@ public class ModelViewer extends JmeCanvasApp -halfLength, -halfLength + yy*GRID_SPACING, 0f); points[idx++] = new Vector3f( +halfLength, -halfLength + yy*GRID_SPACING, 0f); - + } Line grid = new Line("grid", points, null, null, null); grid.getBatch(0).getDefaultColor().set(0.25f, 0.25f, 0.25f, 1f); @@ -475,7 +477,7 @@ public class ModelViewer extends JmeCanvasApp grid.updateModelBound(); _ctx.getGeometry().attachChild(grid); grid.updateRenderState(); - + // attach a dummy node to draw debugging views _ctx.getGeometry().attachChild(new Node("debug") { public void onDraw (Renderer r) { @@ -494,7 +496,7 @@ public class ModelViewer extends JmeCanvasApp } }); } - + /** * Draws the pivot axes of the given node and its children. */ @@ -519,7 +521,7 @@ public class ModelViewer extends JmeCanvasApp _axes.getWorldTranslation().set(spatial.getWorldTranslation()); _axes.getWorldRotation().set(spatial.getWorldRotation()); _axes.draw(r); - + if (spatial instanceof Node) { Node node = (Node)spatial; for (int ii = 0, nn = node.getQuantity(); ii < nn; ii++) { @@ -527,7 +529,7 @@ public class ModelViewer extends JmeCanvasApp } } } - + @Override // documentation inherited protected void initLighting () { @@ -535,13 +537,13 @@ public class ModelViewer extends JmeCanvasApp _dlight.setEnabled(true); _dlight.getDirection().set(-1f, 0f, -1f).normalizeLocal(); _dlight.getAmbient().set(0.25f, 0.25f, 0.25f, 1f); - + LightState lstate = _ctx.getRenderer().createLightState(); lstate.attach(_dlight); _ctx.getGeometry().setRenderState(lstate); _ctx.getGeometry().setLightCombineMode(LightState.REPLACE); } - + /** * Shows the load model dialog. */ @@ -573,7 +575,7 @@ public class ModelViewer extends JmeCanvasApp } _config.setValue("dir", _chooser.getCurrentDirectory().toString()); } - + /** * Attempts to load a model from the specified location. */ @@ -584,19 +586,19 @@ public class ModelViewer extends JmeCanvasApp if (fpath.endsWith(".properties")) { compileModel(file); } else if (fpath.endsWith(".dat")) { - loadCompiledModel(file); + loadCompiledModel(file); } else { throw new Exception(_msg.get("m.invalid_type")); } _status.setText(_msg.get("m.loaded_model", fpath)); _loaded = file; - + } catch (Exception e) { e.printStackTrace(); _status.setText(_msg.get("m.load_error", fpath, e)); } } - + /** * Attempts to compile and load a model. */ @@ -616,7 +618,7 @@ public class ModelViewer extends JmeCanvasApp fpath = (didx == -1) ? fpath : fpath.substring(0, didx); loadCompiledModel(new File(fpath + ".dat")); } - + /** * Attempts to load a model that has already been compiled. */ @@ -626,7 +628,7 @@ public class ModelViewer extends JmeCanvasApp _status.setText(_msg.get("m.loading_model", file)); setModel(Model.readFromFile(file), file); } - + /** * Sets the model once it's been loaded. * @@ -639,16 +641,17 @@ public class ModelViewer extends JmeCanvasApp } _ctx.getGeometry().attachChild(_model = _omodel = model); _model.setAnimationMode(_animMode); + _model.configureShaders(_scache); _model.lockStaticMeshes(_ctx.getRenderer(), true, true); - + // load the model's textures resolveModelTextures(file); - + // recenter the camera _model.updateGeometricState(0f, true); ((OrbitCameraHandler)_camhand).recenter(); updateCameraPosition(); - + // configure the variant menu _variant = null; _vmenu.removeAll(); @@ -685,7 +688,7 @@ public class ModelViewer extends JmeCanvasApp DefaultComboBoxModel abmodel = new DefaultComboBoxModel(anims); _animbox.setModel(abmodel); updateAnimationSpeed(); - + // if there are any sequences, add those as well String[] seqs = StringUtil.parseStringArray( _model.getProperties().getProperty("sequences", "")); @@ -693,7 +696,7 @@ public class ModelViewer extends JmeCanvasApp abmodel.addElement(seq); } } - + /** * Switches to the named variant. */ @@ -708,7 +711,7 @@ public class ModelViewer extends JmeCanvasApp resolveModelTextures(_loaded); _variant = variant; } - + /** * Resolve the textures from the file's directory. */ @@ -744,7 +747,7 @@ public class ModelViewer extends JmeCanvasApp }); _model.updateRenderState(); } - + /** * Shows the import particle system dialog. */ @@ -776,7 +779,7 @@ public class ModelViewer extends JmeCanvasApp _config.setValue("import_dir", _ichooser.getCurrentDirectory().toString()); } - + /** * Attempts to import the specified file as a JME binary scene. */ @@ -793,14 +796,14 @@ public class ModelViewer extends JmeCanvasApp new ImportDialog(file, (Spatial)BinaryImporter.getInstance().load( file)).setVisible(true); - + } catch (Exception e) { e.printStackTrace(); _status.setText(_msg.get("m.load_error", file, e)); } TextureKey.setLocationOverride(null); } - + /** * Updates the model's animation speed based on the position of the * animation speed slider. @@ -810,82 +813,85 @@ public class ModelViewer extends JmeCanvasApp _model.setAnimationSpeed( FastMath.pow(2f, _animspeed.getValue() / 25f)); } - + /** The resource manager. */ protected ResourceManager _rsrcmgr; - + + /** The shader cache. */ + protected ShaderCache _scache; + /** The translation bundle. */ protected MessageBundle _msg; - + /** The path of the initial model to load. */ protected String _path; - + /** The last model successfully loaded. */ protected File _loaded; - + /** The viewer frame. */ protected JFrame _frame; - + /** The variant menu. */ protected JMenu _vmenu; - + /** Debug view switches. */ protected JCheckBoxMenuItem _pivots, _bounds, _normals; - + /** The animation controls. */ protected JPanel _animctrls; - + /** The animation selector. */ protected JComboBox _animbox; - + /** The "stop animation" button. */ protected JButton _animstop; - + /** The animation speed slider. */ - protected JSlider _animspeed; - + protected JSlider _animspeed; + /** The status bar. */ protected JLabel _status; - + /** The camera position display. */ protected JLabel _campos; - + /** The model file chooser. */ protected JFileChooser _chooser; - + /** The import file chooser. */ protected JFileChooser _ichooser; - + /** The desired animation mode. */ protected Model.AnimationMode _animMode; - + /** The desired variant. */ protected String _variant; - + /** The light rotation dialog. */ protected RotateLightDialog _rldialog; - + /** The scene light. */ protected DirectionalLight _dlight; - + /** Used to toggle wireframe rendering. */ protected WireframeState _wfstate; - + /** The currently loaded model. */ protected Model _model; - + /** The original model (before switching to a variant). */ protected Model _omodel; - + /** Reused to draw pivot axes. */ protected Line _axes; - + /** The current animation sequence, if any. */ protected String[] _sequence; - + /** The current index in the animation sequence. */ protected int _seqidx; - + /** Enables and disables the stop button when animations start and stop. */ protected Model.AnimationObserver _animobs = new Model.AnimationObserver() { @@ -913,7 +919,7 @@ public class ModelViewer extends JmeCanvasApp return true; } }; - + /** Allows users to manipulate an imported JME file. */ protected class ImportDialog extends JDialog implements ChangeListener @@ -922,25 +928,25 @@ public class ModelViewer extends JmeCanvasApp { super(_frame, _msg.get("m.import", file), false); _spatial = spatial; - + // rotate from y-up to z-up and set initial scale _spatial.getLocalRotation().fromAngleNormalAxis(FastMath.HALF_PI, Vector3f.UNIT_X); _spatial.setLocalScale(0.025f); - + JPanel cpanel = GroupLayout.makeVBox(); getContentPane().add(cpanel, BorderLayout.CENTER); - + JPanel spanel = new JPanel(); spanel.add(new JLabel(_msg.get("m.scale"))); spanel.add(_scale = new JSlider(0, 1000, 250)); _scale.addChangeListener(this); cpanel.add(spanel); - + JPanel bpanel = new JPanel(); bpanel.add(new JButton(new AbstractAction( _msg.get("m.respawn_particles")) { - public void actionPerformed (ActionEvent e) { + public void actionPerformed (ActionEvent e) { _respawner.traverse(_spatial); } })); @@ -952,13 +958,13 @@ public class ModelViewer extends JmeCanvasApp getContentPane().add(bpanel, BorderLayout.SOUTH); pack(); } - + // documentation inherited from interface ChangeListener public void stateChanged (ChangeEvent e) { _spatial.setLocalScale(_scale.getValue() * 0.0001f); } - + @Override // documentation inherited public void setVisible (boolean visible) { @@ -970,14 +976,14 @@ public class ModelViewer extends JmeCanvasApp _ctx.getGeometry().detachChild(_spatial); } } - + /** The imported scene. */ protected Spatial _spatial; - + /** The scale slider. */ protected JSlider _scale; } - + /** Allows users to move the directional light around. */ protected class RotateLightDialog extends JDialog implements ChangeListener @@ -985,22 +991,22 @@ public class ModelViewer extends JmeCanvasApp public RotateLightDialog () { super(_frame, _msg.get("m.rotate_light"), false); - + JPanel cpanel = GroupLayout.makeVBox(); getContentPane().add(cpanel, BorderLayout.CENTER); - + JPanel apanel = new JPanel(); apanel.add(new JLabel(_msg.get("m.azimuth"))); apanel.add(_azimuth = new JSlider(-180, +180, 0)); _azimuth.addChangeListener(this); cpanel.add(apanel); - + JPanel epanel = new JPanel(); epanel.add(new JLabel(_msg.get("m.elevation"))); epanel.add(_elevation = new JSlider(-90, +90, 45)); _elevation.addChangeListener(this); cpanel.add(epanel); - + JPanel bpanel = new JPanel(); bpanel.add(new JButton(new AbstractAction(_msg.get("m.close")) { public void actionPerformed (ActionEvent e) { @@ -1009,7 +1015,7 @@ public class ModelViewer extends JmeCanvasApp })); getContentPane().add(bpanel, BorderLayout.SOUTH); } - + // documentation inherited from interface ChangeListener public void stateChanged (ChangeEvent e) { @@ -1020,11 +1026,11 @@ public class ModelViewer extends JmeCanvasApp -FastMath.sin(az) * FastMath.cos(el), -FastMath.sin(el)); } - + /** Azimuth and elevation sliders. */ protected JSlider _azimuth, _elevation; } - + /** Moves the camera using mouse input. */ protected class MouseOrbiter extends MouseAdapter implements MouseMotionListener, MouseWheelListener @@ -1034,12 +1040,12 @@ public class ModelViewer extends JmeCanvasApp { _mloc.setLocation(e.getX(), e.getY()); } - + // documentation inherited from interface MouseMotionListener public void mouseMoved (MouseEvent e) { } - + // documentation inherited from interface MouseMotionListener public void mouseDragged (MouseEvent e) { @@ -1056,18 +1062,18 @@ public class ModelViewer extends JmeCanvasApp } updateCameraPosition(); } - + // documentation inherited from interface MouseWheelListener public void mouseWheelMoved (MouseWheelEvent e) { _camhand.zoomCamera(e.getWheelRotation() * 10f); updateCameraPosition(); } - + /** The last recorded position of the mouse cursor. */ protected Point _mloc = new Point(); } - + /** A camera handler that pans in directions orthogonal to the camera * direction. */ protected class OrbitCameraHandler extends CameraHandler @@ -1077,7 +1083,7 @@ public class ModelViewer extends JmeCanvasApp super(camera); _gpoint = super.getGroundPoint(); } - + @Override // documentation inherited public void panCamera (float x, float y) { Vector3f offset = _camera.getLeft().mult(-x).addLocal( @@ -1086,13 +1092,13 @@ public class ModelViewer extends JmeCanvasApp _camera.getLocation().addLocal(offset); _camera.onFrameChange(); } - + @Override // documentation inherited public Vector3f getGroundPoint () { return _gpoint; } - + /** * Resets the ground point to the center of the grid or, if there is * one, the center of the model. @@ -1111,14 +1117,14 @@ public class ModelViewer extends JmeCanvasApp _camera.onFrameChange(); _gpoint.set(target); } - + /** The point at which the camera is looking. */ protected Vector3f _gpoint; } - + /** The app configuration. */ protected static PrefsConfig _config = new PrefsConfig("com/threerings/jme/tools/ModelViewer"); - + /** Forces all particle systems to respawn. */ protected static SpatialVisitor _respawner = new SpatialVisitor(ParticleGeometry.class) { @@ -1126,13 +1132,13 @@ public class ModelViewer extends JmeCanvasApp geom.forceRespawn(); } }; - + /** The number of lines on the grid in each direction. */ protected static final int GRID_SIZE = 32; - + /** The spacing between lines on the grid. */ protected static final float GRID_SPACING = 2.5f; - + /** The number formal used for the camera position. */ protected static final DecimalFormat CAMPOS_FORMAT = new DecimalFormat("+000.000;-000.000"); diff --git a/src/java/com/threerings/jme/util/JmeUtil.java b/src/java/com/threerings/jme/util/JmeUtil.java index e9ce41c9..70f8cf97 100644 --- a/src/java/com/threerings/jme/util/JmeUtil.java +++ b/src/java/com/threerings/jme/util/JmeUtil.java @@ -22,9 +22,9 @@ package com.threerings.jme.util; import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.IntBuffer; @@ -198,14 +198,16 @@ public class JmeUtil /** * Loads the specified shaders (prepending the supplied preprocessor definitions) - * and returns a GLSL shader state. One of the supplied files may be null + * and returns a GLSL shader state. One of the supplied streams may be null * in order to use the fixed-function pipeline for that part. The method returns * null if the shaders fail to compile (JME will log an error). * * @param defs a number of preprocessor definitions to be #defined in both shaders * (e.g., "ENABLE_FOG", "NUM_LIGHTS 2"). */ - public static GLSLShaderObjectsState loadShaders (File vert, File frag, String... defs) + public static GLSLShaderObjectsState loadShaders ( + InputStream vert, InputStream frag, String... defs) + throws IOException { GLSLShaderObjectsState sstate = DisplaySystem.getDisplaySystem().getRenderer().createGLSLShaderObjectsState(); @@ -223,21 +225,18 @@ public class JmeUtil /** * Loads an entire source file as a string, prepending the supplied preprocessor definitions. */ - protected static String readSource (File file, String[] defs) + protected static String readSource (InputStream in, String[] defs) + throws IOException { StringBuffer buf = new StringBuffer(); String ln = System.getProperty("line.separator"); for (String def : defs) { buf.append("#define ").append(def).append(ln); } - try { - BufferedReader reader = new BufferedReader(new FileReader(file)); - String line; - while ((line = reader.readLine()) != null) { - buf.append(line).append(ln); - } - } catch (IOException e) { - return null; + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line; + while ((line = reader.readLine()) != null) { + buf.append(line).append(ln); } return buf.toString(); } diff --git a/src/java/com/threerings/jme/util/ShaderCache.java b/src/java/com/threerings/jme/util/ShaderCache.java new file mode 100644 index 00000000..dd361cf5 --- /dev/null +++ b/src/java/com/threerings/jme/util/ShaderCache.java @@ -0,0 +1,184 @@ +// +// $Id: ImageCache.java 158 2007-02-24 00:38:17Z mdb $ +// +// Nenya library - tools for developing networked games +// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/nenya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.jme.util; + +import java.io.IOException; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; + +import com.jme.scene.state.GLSLShaderObjectsState; +import com.jme.system.DisplaySystem; +import com.jme.util.ShaderAttribute; +import com.jme.util.ShaderUniform; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.ObjectUtil; + +import com.threerings.resource.ResourceManager; + +import static com.threerings.jme.Log.*; + +/** + * Caches shaders under their names and preprocessor definitions, ensuring that identical shaders + * are only compiled once. + */ +public class ShaderCache +{ + /** + * Create a shader cache that will obtain shader source from the supplied resource manager. + */ + public ShaderCache (ResourceManager rsrcmgr) + { + _rsrcmgr = rsrcmgr; + } + + /** + * Creates a new shader state with the supplied vertex shader, fragment shader, + * and preprocessor definitions. If a program with the given parameters has already been + * compiled, the state will use the program id of the existing shader. + * + * @return the newly created state, or null if there was an error and the + * program could not be compiled. + */ + public GLSLShaderObjectsState createState (String vert, String frag, String... defs) + { + GLSLShaderObjectsState sstate = + DisplaySystem.getDisplaySystem().getRenderer().createGLSLShaderObjectsState(); + return (configureState(sstate, vert, frag, defs) ? sstate : null); + } + + /** + * (Re)configures an existing shader state with the supplied parameters. + * + * @return true if the shader was successfully configured, false if the shader could not + * be compiled. + */ + public boolean configureState ( + GLSLShaderObjectsState sstate, String vert, String frag, String... defs) + { + return configureState(sstate, vert, frag, defs, null); + } + + /** + * (Re)configures an existing shader state with the supplied parameters. + * + * @param ddefs an optional array of derived preprocessor definitions that, unlike the + * principal definitions, need not be compared when differentiating between cached shaders. + * @return true if the shader was successfully configured, false if the shader could not + * be compiled. + */ + public boolean configureState ( + GLSLShaderObjectsState sstate, String vert, String frag, String[] defs, String[] ddefs) + { + ShaderKey key = new ShaderKey(vert, frag, defs); + Integer programId = _programIds.get(key); + if (programId == null) { + if (ddefs != null) { + defs = ArrayUtil.concatenate(defs, ddefs); + } + GLSLShaderObjectsState pstate; + try { + pstate = JmeUtil.loadShaders( + (vert == null) ? null : _rsrcmgr.getResource(vert), + (frag == null) ? null : _rsrcmgr.getResource(frag), defs); + } catch (IOException e) { + log.warning("Failed to load shaders [vert=" + vert + ", frag=" + frag + + ", error=" + e + "]."); + return false; + } + if (pstate == null) { + return false; + } + _programIds.put(key, programId = pstate.getProgramID()); + } + if (sstate.getProgramID() == programId) { + return true; + } + sstate.setProgramID(programId); + for (ShaderAttribute attrib : sstate.attribs.values()) { + attrib.attributeID = -1; + } + for (ShaderUniform uniform : sstate.uniforms.values()) { + uniform.uniformID = -1; + } + sstate.setNeedsRefresh(true); + return true; + } + + /** + * Checks whether the specified shader is already loaded. This is useful in order to avoid + * generating complex derived definitions when they won't be needed. + */ + public boolean isLoaded (String vert, String frag, String... defs) + { + return _programIds.containsKey(new ShaderKey(vert, frag, defs)); + } + + /** Identifies a cached shader. */ + protected static class ShaderKey + { + /** The name of the vertex shader (or null for none). */ + public String vert; + + /** The name of the fragment shader (or null for none). */ + public String frag; + + /** The set of preprocessor definitions. */ + public HashSet defs = new HashSet(); + + public ShaderKey (String vert, String frag, String[] defs) + { + this.vert = vert; + this.frag = frag; + Collections.addAll(this.defs, defs); + } + + @Override // documentation inherited + public int hashCode () + { + return (vert == null ? 0 : vert.hashCode()) + (frag == null ? 0 : frag.hashCode()) + + defs.hashCode(); + } + + @Override // documentation inherited + public boolean equals (Object obj) + { + ShaderKey okey = (ShaderKey)obj; + return ObjectUtil.equals(vert, okey.vert) && ObjectUtil.equals(frag, okey.frag) && + defs.equals(okey.defs); + } + + @Override // documentation inherited + public String toString () + { + return "[vert=" + vert + ", frag=" + frag + ", defs=" + defs + "]"; + } + } + + /** Provides access to shader source. */ + protected ResourceManager _rsrcmgr; + + /** Maps shader keys to linked program ids. */ + protected HashMap _programIds = new HashMap(); +}