1/*
2 * Copyright (c) 2009-2010 jMonkeyEngine
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 * * Redistributions of source code must retain the above copyright
10 *   notice, this list of conditions and the following disclaimer.
11 *
12 * * Redistributions in binary form must reproduce the above copyright
13 *   notice, this list of conditions and the following disclaimer in the
14 *   documentation and/or other materials provided with the distribution.
15 *
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 *   may be used to endorse or promote products derived from this software
18 *   without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32package com.jme3.scene;
33
34import com.jme3.asset.AssetNotFoundException;
35import com.jme3.bounding.BoundingVolume;
36import com.jme3.collision.Collidable;
37import com.jme3.collision.CollisionResults;
38import com.jme3.export.InputCapsule;
39import com.jme3.export.JmeExporter;
40import com.jme3.export.JmeImporter;
41import com.jme3.export.OutputCapsule;
42import com.jme3.material.Material;
43import com.jme3.math.Matrix4f;
44import com.jme3.math.Transform;
45import com.jme3.scene.VertexBuffer.Type;
46import com.jme3.util.TempVars;
47import java.io.IOException;
48import java.util.Queue;
49import java.util.logging.Level;
50import java.util.logging.Logger;
51
52/**
53 * <code>Geometry</code> defines a leaf node of the scene graph. The leaf node
54 * contains the geometric data for rendering objects. It manages all rendering
55 * information such as a {@link Material} object to define how the surface
56 * should be shaded and the {@link Mesh} data to contain the actual geometry.
57 *
58 * @author Kirill Vainer
59 */
60public class Geometry extends Spatial {
61
62    // Version #1: removed shared meshes.
63    // models loaded with shared mesh will be automatically fixed.
64    public static final int SAVABLE_VERSION = 1;
65
66    private static final Logger logger = Logger.getLogger(Geometry.class.getName());
67    protected Mesh mesh;
68    protected transient int lodLevel = 0;
69    protected Material material;
70    /**
71     * When true, the geometry's transform will not be applied.
72     */
73    protected boolean ignoreTransform = false;
74    protected transient Matrix4f cachedWorldMat = new Matrix4f();
75    /**
76     * used when geometry is batched
77     */
78    protected BatchNode batchNode = null;
79    /**
80     * the start index of this geom's mesh in the batchNode mesh
81     */
82    protected int startIndex;
83    /**
84     * the previous transforms of the geometry used to compute world transforms
85     */
86    protected Transform prevBatchTransforms = null;
87    /**
88     * the cached offset matrix used when the geometry is batched
89     */
90    protected Matrix4f cachedOffsetMat = null;
91
92    /**
93     * Serialization only. Do not use.
94     */
95    public Geometry() {
96    }
97
98    /**
99     * Create a geometry node without any mesh data.
100     * Both the mesh and the material are null, the geometry
101     * cannot be rendered until those are set.
102     *
103     * @param name The name of this geometry
104     */
105    public Geometry(String name) {
106        super(name);
107    }
108
109    /**
110     * Create a geometry node with mesh data.
111     * The material of the geometry is null, it cannot
112     * be rendered until it is set.
113     *
114     * @param name The name of this geometry
115     * @param mesh The mesh data for this geometry
116     */
117    public Geometry(String name, Mesh mesh) {
118        this(name);
119        if (mesh == null) {
120            throw new NullPointerException();
121        }
122
123        this.mesh = mesh;
124    }
125
126    /**
127     * @return If ignoreTransform mode is set.
128     *
129     * @see Geometry#setIgnoreTransform(boolean)
130     */
131    public boolean isIgnoreTransform() {
132        return ignoreTransform;
133    }
134
135    /**
136     * @param ignoreTransform If true, the geometry's transform will not be applied.
137     */
138    public void setIgnoreTransform(boolean ignoreTransform) {
139        this.ignoreTransform = ignoreTransform;
140    }
141
142    /**
143     * Sets the LOD level to use when rendering the mesh of this geometry.
144     * Level 0 indicates that the default index buffer should be used,
145     * levels [1, LodLevels + 1] represent the levels set on the mesh
146     * with {@link Mesh#setLodLevels(com.jme3.scene.VertexBuffer[]) }.
147     *
148     * @param lod The lod level to set
149     */
150    @Override
151    public void setLodLevel(int lod) {
152        if (mesh.getNumLodLevels() == 0) {
153            throw new IllegalStateException("LOD levels are not set on this mesh");
154        }
155
156        if (lod < 0 || lod >= mesh.getNumLodLevels()) {
157            throw new IllegalArgumentException("LOD level is out of range: " + lod);
158        }
159
160        lodLevel = lod;
161    }
162
163    /**
164     * Returns the LOD level set with {@link #setLodLevel(int) }.
165     *
166     * @return the LOD level set
167     */
168    public int getLodLevel() {
169        return lodLevel;
170    }
171
172    /**
173     * Returns this geometry's mesh vertex count.
174     *
175     * @return this geometry's mesh vertex count.
176     *
177     * @see Mesh#getVertexCount()
178     */
179    public int getVertexCount() {
180        return mesh.getVertexCount();
181    }
182
183    /**
184     * Returns this geometry's mesh triangle count.
185     *
186     * @return this geometry's mesh triangle count.
187     *
188     * @see Mesh#getTriangleCount()
189     */
190    public int getTriangleCount() {
191        return mesh.getTriangleCount();
192    }
193
194    /**
195     * Sets the mesh to use for this geometry when rendering.
196     *
197     * @param mesh the mesh to use for this geometry
198     *
199     * @throws IllegalArgumentException If mesh is null
200     */
201    public void setMesh(Mesh mesh) {
202        if (mesh == null) {
203            throw new IllegalArgumentException();
204        }
205        if (isBatched()) {
206            throw new UnsupportedOperationException("Cannot set the mesh of a batched geometry");
207        }
208
209        this.mesh = mesh;
210        setBoundRefresh();
211    }
212
213    /**
214     * Returns the mseh to use for this geometry
215     *
216     * @return the mseh to use for this geometry
217     *
218     * @see #setMesh(com.jme3.scene.Mesh)
219     */
220    public Mesh getMesh() {
221        return mesh;
222    }
223
224    /**
225     * Sets the material to use for this geometry.
226     *
227     * @param material the material to use for this geometry
228     */
229    @Override
230    public void setMaterial(Material material) {
231        if (isBatched()) {
232            throw new UnsupportedOperationException("Cannot set the material of a batched geometry, change the material of the parent BatchNode.");
233        }
234        this.material = material;
235    }
236
237    /**
238     * Returns the material that is used for this geometry.
239     *
240     * @return the material that is used for this geometry
241     *
242     * @see #setMaterial(com.jme3.material.Material)
243     */
244    public Material getMaterial() {
245        return material;
246    }
247
248    /**
249     * @return The bounding volume of the mesh, in model space.
250     */
251    public BoundingVolume getModelBound() {
252        return mesh.getBound();
253    }
254
255    /**
256     * Updates the bounding volume of the mesh. Should be called when the
257     * mesh has been modified.
258     */
259    public void updateModelBound() {
260        mesh.updateBound();
261        setBoundRefresh();
262    }
263
264    /**
265     * <code>updateWorldBound</code> updates the bounding volume that contains
266     * this geometry. The location of the geometry is based on the location of
267     * all this node's parents.
268     *
269     * @see Spatial#updateWorldBound()
270     */
271    @Override
272    protected void updateWorldBound() {
273        super.updateWorldBound();
274        if (mesh == null) {
275            throw new NullPointerException("Geometry: " + getName() + " has null mesh");
276        }
277
278        if (mesh.getBound() != null) {
279            if (ignoreTransform) {
280                // we do not transform the model bound by the world transform,
281                // just use the model bound as-is
282                worldBound = mesh.getBound().clone(worldBound);
283            } else {
284                worldBound = mesh.getBound().transform(worldTransform, worldBound);
285            }
286        }
287    }
288
289    @Override
290    protected void updateWorldTransforms() {
291
292        super.updateWorldTransforms();
293        computeWorldMatrix();
294
295        if (isBatched()) {
296            computeOffsetTransform();
297            batchNode.updateSubBatch(this);
298            prevBatchTransforms.set(batchNode.getTransforms(this));
299
300        }
301        // geometry requires lights to be sorted
302        worldLights.sort(true);
303    }
304
305    /**
306     * Batch this geometry, should only be called by the BatchNode.
307     * @param node the batchNode
308     * @param startIndex the starting index of this geometry in the batched mesh
309     */
310    protected void batch(BatchNode node, int startIndex) {
311        this.batchNode = node;
312        this.startIndex = startIndex;
313        prevBatchTransforms = new Transform();
314        cachedOffsetMat = new Matrix4f();
315        setCullHint(CullHint.Always);
316    }
317
318    /**
319     * unBatch this geometry.
320     */
321    protected void unBatch() {
322        this.startIndex = 0;
323        prevBatchTransforms = null;
324        cachedOffsetMat = null;
325        //once the geometry is removed from the screnegraph the batchNode needs to be rebatched.
326        this.batchNode.setNeedsFullRebatch(true);
327        this.batchNode = null;
328        setCullHint(CullHint.Dynamic);
329    }
330
331    @Override
332    public boolean removeFromParent() {
333        boolean removed = super.removeFromParent();
334        //if the geometry is batched we also have to unbatch it
335        if (isBatched()) {
336            unBatch();
337        }
338        return removed;
339    }
340
341    /**
342     * Recomputes the cached offset matrix used when the geometry is batched     *
343     */
344    public void computeOffsetTransform() {
345        TempVars vars = TempVars.get();
346        Matrix4f tmpMat = vars.tempMat42;
347
348        // Compute the cached world matrix
349        cachedOffsetMat.loadIdentity();
350        cachedOffsetMat.setRotationQuaternion(prevBatchTransforms.getRotation());
351        cachedOffsetMat.setTranslation(prevBatchTransforms.getTranslation());
352
353
354        Matrix4f scaleMat = vars.tempMat4;
355        scaleMat.loadIdentity();
356        scaleMat.scale(prevBatchTransforms.getScale());
357        cachedOffsetMat.multLocal(scaleMat);
358        cachedOffsetMat.invertLocal();
359
360        tmpMat.loadIdentity();
361        tmpMat.setRotationQuaternion(batchNode.getTransforms(this).getRotation());
362        tmpMat.setTranslation(batchNode.getTransforms(this).getTranslation());
363        scaleMat.loadIdentity();
364        scaleMat.scale(batchNode.getTransforms(this).getScale());
365        tmpMat.multLocal(scaleMat);
366
367        tmpMat.mult(cachedOffsetMat, cachedOffsetMat);
368
369        vars.release();
370    }
371
372    /**
373     * Indicate that the transform of this spatial has changed and that
374     * a refresh is required.
375     */
376    @Override
377    protected void setTransformRefresh() {
378        refreshFlags |= RF_TRANSFORM;
379        setBoundRefresh();
380    }
381
382    /**
383     * Recomputes the matrix returned by {@link Geometry#getWorldMatrix() }.
384     * This will require a localized transform update for this geometry.
385     */
386    public void computeWorldMatrix() {
387        // Force a local update of the geometry's transform
388        checkDoTransformUpdate();
389
390        // Compute the cached world matrix
391        cachedWorldMat.loadIdentity();
392        cachedWorldMat.setRotationQuaternion(worldTransform.getRotation());
393        cachedWorldMat.setTranslation(worldTransform.getTranslation());
394
395        TempVars vars = TempVars.get();
396        Matrix4f scaleMat = vars.tempMat4;
397        scaleMat.loadIdentity();
398        scaleMat.scale(worldTransform.getScale());
399        cachedWorldMat.multLocal(scaleMat);
400        vars.release();
401    }
402
403    /**
404     * A {@link Matrix4f matrix} that transforms the {@link Geometry#getMesh() mesh}
405     * from model space to world space. This matrix is computed based on the
406     * {@link Geometry#getWorldTransform() world transform} of this geometry.
407     * In order to receive updated values, you must call {@link Geometry#computeWorldMatrix() }
408     * before using this method.
409     *
410     * @return Matrix to transform from local space to world space
411     */
412    public Matrix4f getWorldMatrix() {
413        return cachedWorldMat;
414    }
415
416    /**
417     * Sets the model bound to use for this geometry.
418     * This alters the bound used on the mesh as well via
419     * {@link Mesh#setBound(com.jme3.bounding.BoundingVolume) } and
420     * forces the world bounding volume to be recomputed.
421     *
422     * @param modelBound The model bound to set
423     */
424    @Override
425    public void setModelBound(BoundingVolume modelBound) {
426        this.worldBound = null;
427        mesh.setBound(modelBound);
428        setBoundRefresh();
429
430        // NOTE: Calling updateModelBound() would cause the mesh
431        // to recompute the bound based on the geometry thus making
432        // this call useless!
433        //updateModelBound();
434    }
435
436    public int collideWith(Collidable other, CollisionResults results) {
437        // Force bound to update
438        checkDoBoundUpdate();
439        // Update transform, and compute cached world matrix
440        computeWorldMatrix();
441
442        assert (refreshFlags & (RF_BOUND | RF_TRANSFORM)) == 0;
443
444        if (mesh != null) {
445            // NOTE: BIHTree in mesh already checks collision with the
446            // mesh's bound
447            int prevSize = results.size();
448            int added = mesh.collideWith(other, cachedWorldMat, worldBound, results);
449            int newSize = results.size();
450            for (int i = prevSize; i < newSize; i++) {
451                results.getCollisionDirect(i).setGeometry(this);
452            }
453            return added;
454        }
455        return 0;
456    }
457
458    @Override
459    public void depthFirstTraversal(SceneGraphVisitor visitor) {
460        visitor.visit(this);
461    }
462
463    @Override
464    protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
465    }
466
467    public boolean isBatched() {
468        return batchNode != null;
469    }
470
471    /**
472     * This version of clone is a shallow clone, in other words, the
473     * same mesh is referenced as the original geometry.
474     * Exception: if the mesh is marked as being a software
475     * animated mesh, (bind pose is set) then the positions
476     * and normals are deep copied.
477     */
478    @Override
479    public Geometry clone(boolean cloneMaterial) {
480        Geometry geomClone = (Geometry) super.clone(cloneMaterial);
481        geomClone.cachedWorldMat = cachedWorldMat.clone();
482        if (material != null) {
483            if (cloneMaterial) {
484                geomClone.material = material.clone();
485            } else {
486                geomClone.material = material;
487            }
488        }
489
490        if (mesh != null && mesh.getBuffer(Type.BindPosePosition) != null) {
491            geomClone.mesh = mesh.cloneForAnim();
492        }
493
494        return geomClone;
495    }
496
497    /**
498     * This version of clone is a shallow clone, in other words, the
499     * same mesh is referenced as the original geometry.
500     * Exception: if the mesh is marked as being a software
501     * animated mesh, (bind pose is set) then the positions
502     * and normals are deep copied.
503     */
504    @Override
505    public Geometry clone() {
506        return clone(true);
507    }
508
509    /**
510     * Creates a deep clone of the geometry,
511     * this creates an identical copy of the mesh
512     * with the vertexbuffer data duplicated.
513     */
514    @Override
515    public Spatial deepClone() {
516        Geometry geomClone = clone(true);
517        geomClone.mesh = mesh.deepClone();
518        return geomClone;
519    }
520
521    @Override
522    public void write(JmeExporter ex) throws IOException {
523        super.write(ex);
524        OutputCapsule oc = ex.getCapsule(this);
525        oc.write(mesh, "mesh", null);
526        if (material != null) {
527            oc.write(material.getAssetName(), "materialName", null);
528        }
529        oc.write(material, "material", null);
530        oc.write(ignoreTransform, "ignoreTransform", false);
531    }
532
533    @Override
534    public void read(JmeImporter im) throws IOException {
535        super.read(im);
536        InputCapsule ic = im.getCapsule(this);
537        mesh = (Mesh) ic.readSavable("mesh", null);
538
539        material = null;
540        String matName = ic.readString("materialName", null);
541        if (matName != null) {
542            // Material name is set,
543            // Attempt to load material via J3M
544            try {
545                material = im.getAssetManager().loadMaterial(matName);
546            } catch (AssetNotFoundException ex) {
547                // Cannot find J3M file.
548                logger.log(Level.FINE, "Cannot locate {0} for geometry {1}", new Object[]{matName, key});
549            }
550        }
551        // If material is NULL, try to load it from the geometry
552        if (material == null) {
553            material = (Material) ic.readSavable("material", null);
554        }
555        ignoreTransform = ic.readBoolean("ignoreTransform", false);
556
557        if (ic.getSavableVersion(Geometry.class) == 0){
558            // Fix shared mesh (if set)
559            Mesh sharedMesh = getUserData(UserData.JME_SHAREDMESH);
560            if (sharedMesh != null){
561                getMesh().extractVertexData(sharedMesh);
562            }
563        }
564    }
565}
566