• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

Main repository of MikuMikuStudio


Commit MetaInfo

Revision654ba48c1411cde75816cfb78e800410438e66ac (tree)
Time2013-07-07 06:22:17
Authorkobayasi <kobayasi@pscn...>
Commiterkobayasi

Log Message

merge from jme10694

Change Summary

Incremental Difference

--- a/engine/src/core/com/jme3/scene/BatchNode.java
+++ b/engine/src/core/com/jme3/scene/BatchNode.java
@@ -1,5 +1,5 @@
11 /*
2- * Copyright (c) 2009-2011 jMonkeyEngine
2+ * Copyright (c) 2009-2012 jMonkeyEngine
33 * All rights reserved.
44 *
55 * Redistribution and use in source and binary forms, with or without
@@ -31,17 +31,13 @@
3131 */
3232 package com.jme3.scene;
3333
34-import com.jme3.export.InputCapsule;
35-import com.jme3.export.JmeExporter;
36-import com.jme3.export.JmeImporter;
37-import com.jme3.export.OutputCapsule;
38-import com.jme3.export.Savable;
34+import com.jme3.export.*;
3935 import com.jme3.material.Material;
4036 import com.jme3.math.Matrix4f;
4137 import com.jme3.math.Transform;
4238 import com.jme3.math.Vector3f;
4339 import com.jme3.scene.mesh.IndexBuffer;
44-import com.jme3.util.IntMap.Entry;
40+import com.jme3.util.SafeArrayList;
4541 import com.jme3.util.TempVars;
4642 import java.io.IOException;
4743 import java.nio.Buffer;
@@ -54,28 +50,41 @@ import java.util.logging.Level;
5450 import java.util.logging.Logger;
5551
5652 /**
57- * BatchNode holds geometrie that are batched version of all geometries that are in its sub scenegraph.
53+ * BatchNode holds geometries that are a batched version of all the geometries that are in its sub scenegraph.
5854 * There is one geometry per different material in the sub tree.
59- * this geometries are directly attached to the node in the scene graph.
60- * usage is like any other node except you have to call the {@link #batch()} method once all geoms have been attached to the sub scene graph and theire material set
55+ * The geometries are directly attached to the node in the scene graph.
56+ * Usage is like any other node except you have to call the {@link #batch()} method once all the geometries have been attached to the sub scene graph and their material set
6157 * (see todo more automagic for further enhancements)
62- * all the geometry that have been batched are set to {@link CullHint#Always} to not render them.
63- * the sub geometries can be transformed as usual their transforms are used to update the mesh of the geometryBatch.
64- * sub geoms can be removed but it may be slower than the normal spatial removing
65- * Sub geoms can be added after the batch() method has been called but won't be batched and will be rendered as normal geometries.
58+ * All the geometries that have been batched are set to {@link CullHint#Always} to not render them.
59+ * The sub geometries can be transformed as usual, their transforms are used to update the mesh of the geometryBatch.
60+ * Sub geoms can be removed but it may be slower than the normal spatial removing
61+ * Sub geoms can be added after the batch() method has been called but won't be batched and will just be rendered as normal geometries.
6662 * To integrate them in the batch you have to call the batch() method again on the batchNode.
6763 *
6864 * TODO normal or tangents or both looks a bit weird
69- * TODO more automagic (batch when needed in the updateLigicalState)
65+ * TODO more automagic (batch when needed in the updateLogicalState)
7066 * @author Nehon
7167 */
7268 public class BatchNode extends Node implements Savable {
7369
7470 private static final Logger logger = Logger.getLogger(BatchNode.class.getName());
7571 /**
76- * the map of geometry holding the batched meshes
72+ * the list of geometry holding the batched meshes
7773 */
78- protected Map<Material, Batch> batches = new HashMap<Material, Batch>();
74+ protected SafeArrayList<Batch> batches = new SafeArrayList<Batch>(Batch.class);
75+ /**
76+ * a map storing he batches by geometry to quickly acces the batch when updating
77+ */
78+ protected Map<Geometry, Batch> batchesByGeom = new HashMap<Geometry, Batch>();
79+ /**
80+ * used to store transformed vectors before proceeding to a bulk put into the FloatBuffer
81+ */
82+ private float[] tmpFloat;
83+ private float[] tmpFloatN;
84+ private float[] tmpFloatT;
85+ int maxVertCount = 0;
86+ boolean useTangents = false;
87+ boolean needsFullRebatch = true;
7988
8089 /**
8190 * Construct a batchNode
@@ -111,9 +120,9 @@ public class BatchNode extends Node implements Savable {
111120 child.updateGeometricState();
112121 }
113122
114- for (Batch batch : batches.values()) {
123+ for (Batch batch : batches.getArray()) {
115124 if (batch.needMeshUpdate) {
116- batch.geometry.getMesh().updateBound();
125+ batch.geometry.updateModelBound();
117126 batch.geometry.updateWorldBound();
118127 batch.needMeshUpdate = false;
119128
@@ -129,31 +138,42 @@ public class BatchNode extends Node implements Savable {
129138
130139 assert refreshFlags == 0;
131140 }
132-
133- protected Transform getTransforms(Geometry geom){
134- return geom.getWorldTransform();
135- }
136141
142+ protected Matrix4f getTransformMatrix(Geometry g){
143+ return g.cachedWorldMat;
144+ }
145+
137146 protected void updateSubBatch(Geometry bg) {
138- Batch batch = batches.get(bg.getMaterial());
147+ Batch batch = batchesByGeom.get(bg);
139148 if (batch != null) {
140149 Mesh mesh = batch.geometry.getMesh();
141-
142- FloatBuffer buf = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.Position).getData();
143- doTransformVerts(buf, 0, bg.startIndex, bg.startIndex + bg.getVertexCount(), buf, bg.cachedOffsetMat);
144- mesh.getBuffer(VertexBuffer.Type.Position).updateData(buf);
145-
146- buf = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.Normal).getData();
147- doTransformNorm(buf, 0, bg.startIndex, bg.startIndex + bg.getVertexCount(), buf, bg.cachedOffsetMat);
148- mesh.getBuffer(VertexBuffer.Type.Normal).updateData(buf);
149-
150-
150+ Mesh origMesh = bg.getMesh();
151+
152+ VertexBuffer pvb = mesh.getBuffer(VertexBuffer.Type.Position);
153+ FloatBuffer posBuf = (FloatBuffer) pvb.getData();
154+ VertexBuffer nvb = mesh.getBuffer(VertexBuffer.Type.Normal);
155+ FloatBuffer normBuf = (FloatBuffer) nvb.getData();
156+
157+ VertexBuffer opvb = origMesh.getBuffer(VertexBuffer.Type.Position);
158+ FloatBuffer oposBuf = (FloatBuffer) opvb.getData();
159+ VertexBuffer onvb = origMesh.getBuffer(VertexBuffer.Type.Normal);
160+ FloatBuffer onormBuf = (FloatBuffer) onvb.getData();
161+ Matrix4f transformMat = getTransformMatrix(bg);
162+
151163 if (mesh.getBuffer(VertexBuffer.Type.Tangent) != null) {
152164
153- buf = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.Tangent).getData();
154- doTransformNorm(buf, 0, bg.startIndex, bg.startIndex + bg.getVertexCount(), buf, bg.cachedOffsetMat);
155- mesh.getBuffer(VertexBuffer.Type.Tangent).updateData(buf);
165+ VertexBuffer tvb = mesh.getBuffer(VertexBuffer.Type.Tangent);
166+ FloatBuffer tanBuf = (FloatBuffer) tvb.getData();
167+ VertexBuffer otvb = origMesh.getBuffer(VertexBuffer.Type.Tangent);
168+ FloatBuffer otanBuf = (FloatBuffer) otvb.getData();
169+ doTransformsTangents(oposBuf, onormBuf, otanBuf, posBuf, normBuf, tanBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
170+ tvb.updateData(tanBuf);
171+ } else {
172+ doTransforms(oposBuf, onormBuf, posBuf, normBuf, bg.startIndex, bg.startIndex + bg.getVertexCount(), transformMat);
156173 }
174+ pvb.updateData(posBuf);
175+ nvb.updateData(normBuf);
176+
157177
158178 batch.needMeshUpdate = true;
159179 }
@@ -166,26 +186,55 @@ public class BatchNode extends Node implements Savable {
166186 public void batch() {
167187 doBatch();
168188 //we set the batch geometries to ignore transforms to avoid transforms of parent nodes to be applied twice
169- for (Batch batch : batches.values()) {
189+ for (Batch batch : batches.getArray()) {
170190 batch.geometry.setIgnoreTransform(true);
191+ batch.geometry.setUserData(UserData.JME_PHYSICSIGNORE, true);
171192 }
193+ updateGeometricState();
172194 }
173195
174196 protected void doBatch() {
175- ///List<Geometry> tmpList = new ArrayList<Geometry>();
176- Map<Material, List<Geometry>> matMap = new HashMap<Material, List<Geometry>>();
177-
178- gatherGeomerties(matMap, this);
179- batches.clear();
197+ Map<Material, List<Geometry>> matMap = new HashMap<Material, List<Geometry>>();
180198 int nbGeoms = 0;
181- for (Material material : matMap.keySet()) {
199+
200+ gatherGeomerties(matMap, this, needsFullRebatch);
201+ if (needsFullRebatch) {
202+ for (Batch batch : batches.getArray()) {
203+ batch.geometry.removeFromParent();
204+ }
205+ batches.clear();
206+ batchesByGeom.clear();
207+ }
208+ //only reset maxVertCount if there is something new to batch
209+ if (matMap.size() > 0) {
210+ maxVertCount = 0;
211+ }
212+
213+ for (Map.Entry<Material, List<Geometry>> entry : matMap.entrySet()) {
182214 Mesh m = new Mesh();
183- List<Geometry> list = matMap.get(material);
215+ Material material = entry.getKey();
216+ List<Geometry> list = entry.getValue();
184217 nbGeoms += list.size();
218+ String batchName = name + "-batch" + batches.size();
219+ Batch batch;
220+ if (!needsFullRebatch) {
221+ batch = findBatchByMaterial(material);
222+ if (batch != null) {
223+ list.add(0, batch.geometry);
224+ batchName = batch.geometry.getName();
225+ batch.geometry.removeFromParent();
226+ } else {
227+ batch = new Batch();
228+ }
229+ } else {
230+ batch = new Batch();
231+ }
185232 mergeGeometries(m, list);
186- Batch batch = new Batch();
233+ m.setDynamic();
234+
235+ batch.updateGeomList(list);
187236
188- batch.geometry = new Geometry(name + "-batch" + batches.size());
237+ batch.geometry = new Geometry(batchName);
189238 batch.geometry.setMaterial(material);
190239 this.attachChild(batch.geometry);
191240
@@ -193,25 +242,81 @@ public class BatchNode extends Node implements Savable {
193242 batch.geometry.setMesh(m);
194243 batch.geometry.getMesh().updateCounts();
195244 batch.geometry.getMesh().updateBound();
196- batches.put(material, batch);
245+ batches.add(batch);
246+ }
247+ if (batches.size() > 0) {
248+ needsFullRebatch = false;
249+ }
250+
251+
252+ logger.log(Level.FINE, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()});
253+
254+ //init the temp arrays if something has been batched only.
255+ if(matMap.size()>0){
256+ //TODO these arrays should be allocated by chunk instead to avoid recreating them each time the batch is changed.
257+ //init temp float arrays
258+ tmpFloat = new float[maxVertCount * 3];
259+ tmpFloatN = new float[maxVertCount * 3];
260+ if (useTangents) {
261+ tmpFloatT = new float[maxVertCount * 4];
262+ }
197263 }
198- logger.log(Level.INFO, "Batched {0} geometries in {1} batches.", new Object[]{nbGeoms, batches.size()});
199264 }
200265
201- private void gatherGeomerties(Map<Material, List<Geometry>> map, Spatial n) {
266+ //in case the detached spatial is a node, we unbatch all geometries in its subegraph
267+ @Override
268+ public Spatial detachChildAt(int index) {
269+ Spatial s = super.detachChildAt(index);
270+ if (s instanceof Node) {
271+ unbatchSubGraph(s);
272+ }
273+ return s;
274+ }
202275
203- if (n.getClass() == Geometry.class) {
204- if (!isBatch(n)) {
276+ /**
277+ * recursively visit the subgraph and unbatch geometries
278+ * @param s
279+ */
280+ private void unbatchSubGraph(Spatial s) {
281+ if (s instanceof Node) {
282+ for (Spatial sp : ((Node) s).getChildren()) {
283+ unbatchSubGraph(sp);
284+ }
285+ } else if (s instanceof Geometry) {
286+ Geometry g = (Geometry) s;
287+ if (g.isBatched()) {
288+ g.unBatch();
289+ }
290+ }
291+ }
292+
293+
294+ private void gatherGeomerties(Map<Material, List<Geometry>> map, Spatial n, boolean rebatch) {
295+
296+ if (n instanceof Geometry) {
297+
298+ if (!isBatch(n) && n.getBatchHint() != BatchHint.Never) {
205299 Geometry g = (Geometry) n;
206- if (g.getMaterial() == null) {
207- throw new IllegalStateException("No material is set for Geometry: " + g.getName() + " please set a material before batching");
208- }
209- List<Geometry> list = map.get(g.getMaterial());
210- if (list == null) {
211- list = new ArrayList<Geometry>();
212- map.put(g.getMaterial(), list);
300+ if (!g.isBatched() || rebatch) {
301+ if (g.getMaterial() == null) {
302+ throw new IllegalStateException("No material is set for Geometry: " + g.getName() + " please set a material before batching");
303+ }
304+ List<Geometry> list = map.get(g.getMaterial());
305+ if (list == null) {
306+ //trying to compare materials with the isEqual method
307+ for (Map.Entry<Material, List<Geometry>> mat : map.entrySet()) {
308+ if (g.getMaterial().contentEquals(mat.getKey())) {
309+ list = mat.getValue();
310+ }
311+ }
312+ }
313+ if (list == null) {
314+ list = new ArrayList<Geometry>();
315+ map.put(g.getMaterial(), list);
316+ }
317+ g.setTransformRefresh();
318+ list.add(g);
213319 }
214- list.add(g);
215320 }
216321
217322 } else if (n instanceof Node) {
@@ -219,14 +324,23 @@ public class BatchNode extends Node implements Savable {
219324 if (child instanceof BatchNode) {
220325 continue;
221326 }
222- gatherGeomerties(map, child);
327+ gatherGeomerties(map, child, rebatch);
223328 }
224329 }
225330
226331 }
227332
333+ private Batch findBatchByMaterial(Material m) {
334+ for (Batch batch : batches.getArray()) {
335+ if (batch.geometry.getMaterial().contentEquals(m)) {
336+ return batch;
337+ }
338+ }
339+ return null;
340+ }
341+
228342 private boolean isBatch(Spatial s) {
229- for (Batch batch : batches.values()) {
343+ for (Batch batch : batches.getArray()) {
230344 if (batch.geometry == s) {
231345 return true;
232346 }
@@ -259,12 +373,12 @@ public class BatchNode extends Node implements Savable {
259373 */
260374 public Material getMaterial() {
261375 if (!batches.isEmpty()) {
262- Batch b = batches.get(batches.keySet().iterator().next());
376+ Batch b = batches.iterator().next();
263377 return b.geometry.getMaterial();
264378 }
265379 return null;//material;
266380 }
267-
381+
268382 // /**
269383 // * Sets the material to the a specific batch of this BatchNode
270384 // *
@@ -294,7 +408,6 @@ public class BatchNode extends Node implements Savable {
294408 // }
295409 // return null;//material;
296410 // }
297-
298411 @Override
299412 public void write(JmeExporter ex) throws IOException {
300413 super.write(ex);
@@ -347,13 +460,16 @@ public class BatchNode extends Node implements Savable {
347460 int totalVerts = 0;
348461 int totalTris = 0;
349462 int totalLodLevels = 0;
463+ int maxWeights = -1;
350464
351465 Mesh.Mode mode = null;
352466 for (Geometry geom : geometries) {
353467 totalVerts += geom.getVertexCount();
354468 totalTris += geom.getTriangleCount();
355469 totalLodLevels = Math.min(totalLodLevels, geom.getMesh().getNumLodLevels());
356-
470+ if (maxVertCount < geom.getVertexCount()) {
471+ maxVertCount = geom.getVertexCount();
472+ }
357473 Mesh.Mode listMode;
358474 int components;
359475 switch (geom.getMesh().getMode()) {
@@ -377,10 +493,12 @@ public class BatchNode extends Node implements Savable {
377493 throw new UnsupportedOperationException();
378494 }
379495
380- for (Entry<VertexBuffer> entry : geom.getMesh().getBuffers()) {
381- compsForBuf[entry.getKey()] = entry.getValue().getNumComponents();
382- formatForBuf[entry.getKey()] = entry.getValue().getFormat();
496+ for (VertexBuffer vb : geom.getMesh().getBufferList().getArray()) {
497+ compsForBuf[vb.getBufferType().ordinal()] = vb.getNumComponents();
498+ formatForBuf[vb.getBufferType().ordinal()] = vb.getFormat();
383499 }
500+
501+ maxWeights = Math.max(maxWeights, geom.getMesh().getMaxNumWeights());
384502
385503 if (mode != null && mode != listMode) {
386504 throw new UnsupportedOperationException("Cannot combine different"
@@ -390,6 +508,7 @@ public class BatchNode extends Node implements Savable {
390508 compsForBuf[VertexBuffer.Type.Index.ordinal()] = components;
391509 }
392510
511+ outMesh.setMaxNumWeights(maxWeights);
393512 outMesh.setMode(mode);
394513 if (totalVerts >= 65536) {
395514 // make sure we create an UnsignedInt buffer so
@@ -413,7 +532,7 @@ public class BatchNode extends Node implements Savable {
413532 }
414533
415534 VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.values()[i]);
416- vb.setupData(VertexBuffer.Usage.Static, compsForBuf[i], formatForBuf[i], data);
535+ vb.setupData(VertexBuffer.Usage.Dynamic, compsForBuf[i], formatForBuf[i], data);
417536 outMesh.setBuffer(vb);
418537 }
419538
@@ -422,13 +541,16 @@ public class BatchNode extends Node implements Savable {
422541
423542 for (Geometry geom : geometries) {
424543 Mesh inMesh = geom.getMesh();
425- geom.batch(this, globalVertIndex);
544+ if (!isBatch(geom)) {
545+ geom.batch(this, globalVertIndex);
546+ }
426547
427548 int geomVertCount = inMesh.getVertexCount();
428549 int geomTriCount = inMesh.getTriangleCount();
429550
430551 for (int bufType = 0; bufType < compsForBuf.length; bufType++) {
431552 VertexBuffer inBuf = inMesh.getBuffer(VertexBuffer.Type.values()[bufType]);
553+
432554 VertexBuffer outBuf = outMesh.getBuffer(VertexBuffer.Type.values()[bufType]);
433555
434556 if (outBuf == null) {
@@ -450,16 +572,20 @@ public class BatchNode extends Node implements Savable {
450572 } else if (VertexBuffer.Type.Position.ordinal() == bufType) {
451573 FloatBuffer inPos = (FloatBuffer) inBuf.getData();
452574 FloatBuffer outPos = (FloatBuffer) outBuf.getData();
453- doCopyBuffer(inPos, globalVertIndex, outPos);
575+ doCopyBuffer(inPos, globalVertIndex, outPos, 3);
454576 } else if (VertexBuffer.Type.Normal.ordinal() == bufType || VertexBuffer.Type.Tangent.ordinal() == bufType) {
455577 FloatBuffer inPos = (FloatBuffer) inBuf.getData();
456578 FloatBuffer outPos = (FloatBuffer) outBuf.getData();
457- doCopyBuffer(inPos, globalVertIndex, outPos);
458- } else {
459- for (int vert = 0; vert < geomVertCount; vert++) {
460- int curGlobalVertIndex = globalVertIndex + vert;
461- inBuf.copyElement(vert, outBuf, curGlobalVertIndex);
579+ doCopyBuffer(inPos, globalVertIndex, outPos, compsForBuf[bufType]);
580+ if (VertexBuffer.Type.Tangent.ordinal() == bufType) {
581+ useTangents = true;
462582 }
583+ } else {
584+ inBuf.copyElements(0, outBuf, globalVertIndex, geomVertCount);
585+// for (int vert = 0; vert < geomVertCount; vert++) {
586+// int curGlobalVertIndex = globalVertIndex + vert;
587+// inBuf.copyElement(vert, outBuf, curGlobalVertIndex);
588+// }
463589 }
464590 }
465591
@@ -468,75 +594,164 @@ public class BatchNode extends Node implements Savable {
468594 }
469595 }
470596
471- private void doTransformVerts(FloatBuffer inBuf, int offset, int start, int end, FloatBuffer outBuf, Matrix4f transform) {
597+ private void doTransforms(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bufPos, FloatBuffer bufNorm, int start, int end, Matrix4f transform) {
472598 TempVars vars = TempVars.get();
473599 Vector3f pos = vars.vect1;
600+ Vector3f norm = vars.vect2;
601+
602+ int length = (end - start) * 3;
474603
475604 // offset is given in element units
476605 // convert to be in component units
477- offset *= 3;
478-
479- for (int i = start; i < end; i++) {
480- int index = i * 3;
481- pos.x = inBuf.get(index);
482- pos.y = inBuf.get(index + 1);
483- pos.z = inBuf.get(index + 2);
606+ int offset = start * 3;
607+ bindBufPos.rewind();
608+ bindBufNorm.rewind();
609+ //bufPos.position(offset);
610+ //bufNorm.position(offset);
611+ bindBufPos.get(tmpFloat, 0, length);
612+ bindBufNorm.get(tmpFloatN, 0, length);
613+ int index = 0;
614+ while (index < length) {
615+ pos.x = tmpFloat[index];
616+ norm.x = tmpFloatN[index++];
617+ pos.y = tmpFloat[index];
618+ norm.y = tmpFloatN[index++];
619+ pos.z = tmpFloat[index];
620+ norm.z = tmpFloatN[index];
484621
485622 transform.mult(pos, pos);
486- index += offset;
487- outBuf.put(index, pos.x);
488- outBuf.put(index + 1, pos.y);
489- outBuf.put(index + 2, pos.z);
623+ transform.multNormal(norm, norm);
624+
625+ index -= 2;
626+ tmpFloat[index] = pos.x;
627+ tmpFloatN[index++] = norm.x;
628+ tmpFloat[index] = pos.y;
629+ tmpFloatN[index++] = norm.y;
630+ tmpFloat[index] = pos.z;
631+ tmpFloatN[index++] = norm.z;
632+
490633 }
491634 vars.release();
635+ bufPos.position(offset);
636+ //using bulk put as it's faster
637+ bufPos.put(tmpFloat, 0, length);
638+ bufNorm.position(offset);
639+ //using bulk put as it's faster
640+ bufNorm.put(tmpFloatN, 0, length);
492641 }
493642
494- private void doTransformNorm(FloatBuffer inBuf, int offset, int start, int end, FloatBuffer outBuf, Matrix4f transform) {
643+ private void doTransformsTangents(FloatBuffer bindBufPos, FloatBuffer bindBufNorm, FloatBuffer bindBufTangents,FloatBuffer bufPos, FloatBuffer bufNorm, FloatBuffer bufTangents, int start, int end, Matrix4f transform) {
495644 TempVars vars = TempVars.get();
496645 Vector3f pos = vars.vect1;
646+ Vector3f norm = vars.vect2;
647+ Vector3f tan = vars.vect3;
648+
649+ int length = (end - start) * 3;
650+ int tanLength = (end - start) * 4;
497651
498652 // offset is given in element units
499653 // convert to be in component units
500- offset *= 3;
501-
502- for (int i = start; i < end; i++) {
503- int index = i * 3;
504- pos.x = inBuf.get(index);
505- pos.y = inBuf.get(index + 1);
506- pos.z = inBuf.get(index + 2);
507-
508- transform.multNormal(pos, pos);
509- index += offset;
510- outBuf.put(index, pos.x);
511- outBuf.put(index + 1, pos.y);
512- outBuf.put(index + 2, pos.z);
654+ int offset = start * 3;
655+ int tanOffset = start * 4;
656+
657+
658+ bindBufPos.rewind();
659+ bindBufNorm.rewind();
660+ bindBufTangents.rewind();
661+ bindBufPos.get(tmpFloat, 0, length);
662+ bindBufNorm.get(tmpFloatN, 0, length);
663+ bindBufTangents.get(tmpFloatT, 0, tanLength);
664+
665+ int index = 0;
666+ int tanIndex = 0;
667+ while (index < length) {
668+ pos.x = tmpFloat[index];
669+ norm.x = tmpFloatN[index++];
670+ pos.y = tmpFloat[index];
671+ norm.y = tmpFloatN[index++];
672+ pos.z = tmpFloat[index];
673+ norm.z = tmpFloatN[index];
674+
675+ tan.x = tmpFloatT[tanIndex++];
676+ tan.y = tmpFloatT[tanIndex++];
677+ tan.z = tmpFloatT[tanIndex++];
678+
679+ transform.mult(pos, pos);
680+ transform.multNormal(norm, norm);
681+ transform.multNormal(tan, tan);
682+
683+ index -= 2;
684+ tanIndex -= 3;
685+
686+ tmpFloat[index] = pos.x;
687+ tmpFloatN[index++] = norm.x;
688+ tmpFloat[index] = pos.y;
689+ tmpFloatN[index++] = norm.y;
690+ tmpFloat[index] = pos.z;
691+ tmpFloatN[index++] = norm.z;
692+
693+ tmpFloatT[tanIndex++] = tan.x;
694+ tmpFloatT[tanIndex++] = tan.y;
695+ tmpFloatT[tanIndex++] = tan.z;
696+
697+ //Skipping 4th element of tangent buffer (handedness)
698+ tanIndex++;
699+
513700 }
514701 vars.release();
702+ bufPos.position(offset);
703+ //using bulk put as it's faster
704+ bufPos.put(tmpFloat, 0, length);
705+ bufNorm.position(offset);
706+ //using bulk put as it's faster
707+ bufNorm.put(tmpFloatN, 0, length);
708+ bufTangents.position(tanOffset);
709+ //using bulk put as it's faster
710+ bufTangents.put(tmpFloatT, 0, tanLength);
515711 }
516712
517- private void doCopyBuffer(FloatBuffer inBuf, int offset, FloatBuffer outBuf) {
713+ private void doCopyBuffer(FloatBuffer inBuf, int offset, FloatBuffer outBuf, int componentSize) {
518714 TempVars vars = TempVars.get();
519715 Vector3f pos = vars.vect1;
520716
521717 // offset is given in element units
522718 // convert to be in component units
523- offset *= 3;
719+ offset *= componentSize;
524720
525- for (int i = 0; i < inBuf.capacity() / 3; i++) {
526- pos.x = inBuf.get(i * 3 + 0);
527- pos.y = inBuf.get(i * 3 + 1);
528- pos.z = inBuf.get(i * 3 + 2);
721+ for (int i = 0; i < inBuf.limit() / componentSize; i++) {
722+ pos.x = inBuf.get(i * componentSize + 0);
723+ pos.y = inBuf.get(i * componentSize + 1);
724+ pos.z = inBuf.get(i * componentSize + 2);
529725
530- outBuf.put(offset + i * 3 + 0, pos.x);
531- outBuf.put(offset + i * 3 + 1, pos.y);
532- outBuf.put(offset + i * 3 + 2, pos.z);
726+ outBuf.put(offset + i * componentSize + 0, pos.x);
727+ outBuf.put(offset + i * componentSize + 1, pos.y);
728+ outBuf.put(offset + i * componentSize + 2, pos.z);
533729 }
534730 vars.release();
535731 }
536732
537733 protected class Batch {
538734
735+ /**
736+ * update the batchesByGeom map for this batch with the given List of geometries
737+ * @param list
738+ */
739+ void updateGeomList(List<Geometry> list) {
740+ for (Geometry geom : list) {
741+ if (!isBatch(geom)) {
742+ batchesByGeom.put(geom, this);
743+ }
744+ }
745+ }
539746 Geometry geometry;
540747 boolean needMeshUpdate = false;
541748 }
749+
750+ protected void setNeedsFullRebatch(boolean needsFullRebatch) {
751+ this.needsFullRebatch = needsFullRebatch;
752+ }
753+
754+ public int getOffsetIndex(Geometry batchedGeometry) {
755+ return batchedGeometry.startIndex;
756+ }
542757 }
--- a/engine/src/core/com/jme3/scene/Geometry.java
+++ b/engine/src/core/com/jme3/scene/Geometry.java
@@ -1,5 +1,5 @@
11 /*
2- * Copyright (c) 2009-2010 jMonkeyEngine
2+ * Copyright (c) 2009-2012 jMonkeyEngine
33 * All rights reserved.
44 *
55 * Redistribution and use in source and binary forms, with or without
@@ -35,9 +35,9 @@ import com.jme3.asset.AssetNotFoundException;
3535 import com.jme3.bounding.BoundingVolume;
3636 import com.jme3.collision.Collidable;
3737 import com.jme3.collision.CollisionResults;
38+import com.jme3.export.InputCapsule;
3839 import com.jme3.export.JmeExporter;
3940 import com.jme3.export.JmeImporter;
40-import com.jme3.export.InputCapsule;
4141 import com.jme3.export.OutputCapsule;
4242 import com.jme3.material.Material;
4343 import com.jme3.math.Matrix4f;
@@ -59,6 +59,9 @@ import java.util.logging.Logger;
5959 */
6060 public class Geometry extends Spatial {
6161
62+ // Version #1: removed shared meshes.
63+ // models loaded with shared mesh will be automatically fixed.
64+ public static final int SAVABLE_VERSION = 1;
6265 private static final Logger logger = Logger.getLogger(Geometry.class.getName());
6366 protected Mesh mesh;
6467 protected transient int lodLevel = 0;
@@ -75,16 +78,7 @@ public class Geometry extends Spatial {
7578 /**
7679 * the start index of this geom's mesh in the batchNode mesh
7780 */
78- protected int startIndex;
79- /**
80- * the previous transforms of the geometry used to compute world transforms
81- */
82- protected Transform prevBatchTransforms = null;
83- /**
84- * the cached offset matrix used when the geometry is batched
85- */
86- protected Matrix4f cachedOffsetMat = null;
87-
81+ protected int startIndex;
8882 /**
8983 * Serialization only. Do not use.
9084 */
@@ -288,11 +282,8 @@ public class Geometry extends Spatial {
288282 super.updateWorldTransforms();
289283 computeWorldMatrix();
290284
291- if (isBatched()) {
292- computeOffsetTransform();
293- batchNode.updateSubBatch(this);
294- prevBatchTransforms.set(batchNode.getTransforms(this));
295-
285+ if (isBatched()) {
286+ batchNode.updateSubBatch(this);
296287 }
297288 // geometry requires lights to be sorted
298289 worldLights.sort(true);
@@ -305,9 +296,7 @@ public class Geometry extends Spatial {
305296 */
306297 protected void batch(BatchNode node, int startIndex) {
307298 this.batchNode = node;
308- this.startIndex = startIndex;
309- prevBatchTransforms = new Transform();
310- cachedOffsetMat = new Matrix4f();
299+ this.startIndex = startIndex;
311300 setCullHint(CullHint.Always);
312301 }
313302
@@ -316,65 +305,40 @@ public class Geometry extends Spatial {
316305 */
317306 protected void unBatch() {
318307 this.startIndex = 0;
319- prevBatchTransforms = null;
320- cachedOffsetMat = null;
321- //once the geometry is removed from the screnegraph we call batch on the batchNode before unreferencing it.
322- this.batchNode.batch();
323- this.batchNode = null;
308+ //once the geometry is removed from the screnegraph the batchNode needs to be rebatched.
309+ if (batchNode != null) {
310+ this.batchNode.setNeedsFullRebatch(true);
311+ this.batchNode = null;
312+ }
324313 setCullHint(CullHint.Dynamic);
325314 }
326315
327316 @Override
328317 public boolean removeFromParent() {
329- boolean removed = super.removeFromParent();
318+ return super.removeFromParent();
319+ }
320+
321+ @Override
322+ protected void setParent(Node parent) {
323+ super.setParent(parent);
330324 //if the geometry is batched we also have to unbatch it
331- if (isBatched()) {
325+ if (parent == null && isBatched()) {
332326 unBatch();
333327 }
334- return removed;
335328 }
336329
337- /**
338- * Recomputes the cached offset matrix used when the geometry is batched *
339- */
340- public void computeOffsetTransform() {
341- TempVars vars = TempVars.get();
342- Matrix4f tmpMat = vars.tempMat42;
343-
344- // Compute the cached world matrix
345- cachedOffsetMat.loadIdentity();
346- cachedOffsetMat.setRotationQuaternion(prevBatchTransforms.getRotation());
347- cachedOffsetMat.setTranslation(prevBatchTransforms.getTranslation());
348-
349-
350- Matrix4f scaleMat = vars.tempMat4;
351- scaleMat.loadIdentity();
352- scaleMat.scale(prevBatchTransforms.getScale());
353- cachedOffsetMat.multLocal(scaleMat);
354- cachedOffsetMat.invertLocal();
355-
356- tmpMat.loadIdentity();
357- tmpMat.setRotationQuaternion(batchNode.getTransforms(this).getRotation());
358- tmpMat.setTranslation(batchNode.getTransforms(this).getTranslation());
359- scaleMat.loadIdentity();
360- scaleMat.scale(batchNode.getTransforms(this).getScale());
361- tmpMat.multLocal(scaleMat);
362-
363- tmpMat.mult(cachedOffsetMat, cachedOffsetMat);
364-
365- vars.release();
366- }
367330
368331 /**
369332 * Indicate that the transform of this spatial has changed and that
370333 * a refresh is required.
371334 */
372- @Override
373- protected void setTransformRefresh() {
374- refreshFlags |= RF_TRANSFORM;
375- setBoundRefresh();
376- }
377-
335+ // NOTE: Spatial has an identical implementation of this method,
336+ // thus it was commented out.
337+// @Override
338+// protected void setTransformRefresh() {
339+// refreshFlags |= RF_TRANSFORM;
340+// setBoundRefresh();
341+// }
378342 /**
379343 * Recomputes the matrix returned by {@link Geometry#getWorldMatrix() }.
380344 * This will require a localized transform update for this geometry.
@@ -474,6 +438,11 @@ public class Geometry extends Spatial {
474438 @Override
475439 public Geometry clone(boolean cloneMaterial) {
476440 Geometry geomClone = (Geometry) super.clone(cloneMaterial);
441+ //this geometry is batched but the clonned one should not be
442+ if (isBatched()) {
443+ geomClone.batchNode = null;
444+ geomClone.unBatch();
445+ }
477446 geomClone.cachedWorldMat = cachedWorldMat.clone();
478447 if (material != null) {
479448 if (cloneMaterial) {
@@ -541,8 +510,7 @@ public class Geometry extends Spatial {
541510 material = im.getAssetManager().loadMaterial(matName);
542511 } catch (AssetNotFoundException ex) {
543512 // Cannot find J3M file.
544- logger.log(Level.FINE, "Could not load J3M file {0} for Geometry.",
545- matName);
513+ logger.log(Level.FINE, "Cannot locate {0} for geometry {1}", new Object[]{matName, key});
546514 }
547515 }
548516 // If material is NULL, try to load it from the geometry
@@ -550,5 +518,13 @@ public class Geometry extends Spatial {
550518 material = (Material) ic.readSavable("material", null);
551519 }
552520 ignoreTransform = ic.readBoolean("ignoreTransform", false);
521+
522+ if (ic.getSavableVersion(Geometry.class) == 0) {
523+ // Fix shared mesh (if set)
524+ Mesh sharedMesh = getUserData(UserData.JME_SHAREDMESH);
525+ if (sharedMesh != null) {
526+ getMesh().extractVertexData(sharedMesh);
527+ }
528+ }
553529 }
554530 }
--- a/engine/src/core/com/jme3/scene/SimpleBatchNode.java
+++ b/engine/src/core/com/jme3/scene/SimpleBatchNode.java
@@ -1,10 +1,39 @@
11 /*
2- * To change this template, choose Tools | Templates
3- * and open the template in the editor.
2+ * Copyright (c) 2009-2012 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.
431 */
532 package com.jme3.scene;
633
34+import com.jme3.math.Matrix4f;
735 import com.jme3.math.Transform;
36+import com.jme3.util.TempVars;
837
938 /**
1039 *
@@ -37,17 +66,30 @@ public class SimpleBatchNode extends BatchNode {
3766
3867 @Override
3968 protected void setTransformRefresh() {
40-
4169 refreshFlags |= RF_TRANSFORM;
4270 setBoundRefresh();
43- for (Batch batch : batches.values()) {
71+ for (Batch batch : batches.getArray()) {
4472 batch.geometry.setTransformRefresh();
4573 }
4674 }
47-
48- protected Transform getTransforms(Geometry geom){
49- return geom.getLocalTransform();
75+ private Matrix4f cachedLocalMat = new Matrix4f();
76+
77+ @Override
78+ protected Matrix4f getTransformMatrix(Geometry g){
79+ // Compute the Local matrix for the geometry
80+ cachedLocalMat.loadIdentity();
81+ cachedLocalMat.setRotationQuaternion(g.localTransform.getRotation());
82+ cachedLocalMat.setTranslation(g.localTransform.getTranslation());
83+
84+ TempVars vars = TempVars.get();
85+ Matrix4f scaleMat = vars.tempMat4;
86+ scaleMat.loadIdentity();
87+ scaleMat.scale(g.localTransform.getScale());
88+ cachedLocalMat.multLocal(scaleMat);
89+ vars.release();
90+ return cachedLocalMat;
5091 }
92+
5193
5294 @Override
5395 public void batch() {
--- a/engine/src/core/com/jme3/scene/Spatial.java
+++ b/engine/src/core/com/jme3/scene/Spatial.java
@@ -33,6 +33,7 @@ package com.jme3.scene;
3333
3434 import com.jme3.asset.Asset;
3535 import com.jme3.asset.AssetKey;
36+import com.jme3.asset.CloneableSmartAsset;
3637 import com.jme3.bounding.BoundingVolume;
3738 import com.jme3.collision.Collidable;
3839 import com.jme3.export.*;
@@ -50,12 +51,7 @@ import com.jme3.scene.control.Control;
5051 import com.jme3.util.SafeArrayList;
5152 import com.jme3.util.TempVars;
5253 import java.io.IOException;
53-import java.util.ArrayList;
54-import java.util.Collection;
55-import java.util.Collections;
56-import java.util.HashMap;
57-import java.util.LinkedList;
58-import java.util.Queue;
54+import java.util.*;
5955 import java.util.logging.Logger;
6056
6157 /**
@@ -68,7 +64,7 @@ import java.util.logging.Logger;
6864 * @author Joshua Slack
6965 * @version $Revision: 4075 $, $Data$
7066 */
71-public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
67+public abstract class Spatial implements Savable, Cloneable, Collidable, CloneableSmartAsset, Asset {
7268
7369 private static final Logger logger = Logger.getLogger(Spatial.class.getName());
7470
@@ -99,31 +95,47 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
9995 */
10096 Never;
10197 }
98+
99+ /**
100+ * Specifies if this spatial should be batched
101+ */
102+ public enum BatchHint {
103+
104+ /**
105+ * Do whatever our parent does. If no parent, default to {@link #Always}.
106+ */
107+ Inherit,
108+ /**
109+ * This spatial will always be batched when attached to a BatchNode.
110+ */
111+ Always,
112+ /**
113+ * This spatial will never be batched when attached to a BatchNode.
114+ */
115+ Never;
116+ }
102117 /**
103118 * Refresh flag types
104119 */
105120 protected static final int RF_TRANSFORM = 0x01, // need light resort + combine transforms
106- RF_BOUND = 0x02,
107- RF_LIGHTLIST = 0x04; // changes in light lists
121+ RF_BOUND = 0x02,
122+ RF_LIGHTLIST = 0x04; // changes in light lists
108123
109124 protected CullHint cullHint = CullHint.Inherit;
110-
125+ protected BatchHint batchHint = BatchHint.Inherit;
111126 /**
112127 * Spatial's bounding volume relative to the world.
113128 */
114129 protected BoundingVolume worldBound;
115-
116130 /**
117131 * LightList
118132 */
119133 protected LightList localLights;
120134 protected transient LightList worldLights;
121-
122135 /**
123136 * This spatial's name.
124137 */
125138 protected String name;
126-
127139 // scale values
128140 protected transient Camera.FrustumIntersect frustrumIntersects = Camera.FrustumIntersect.Intersects;
129141 protected RenderQueue.Bucket queueBucket = RenderQueue.Bucket.Inherit;
@@ -133,14 +145,12 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
133145 protected Transform worldTransform;
134146 protected SafeArrayList<Control> controls = new SafeArrayList<Control>(Control.class);
135147 protected HashMap<String, Savable> userData = null;
136-
137148 /**
138149 * Used for smart asset caching
139150 *
140151 * @see AssetKey#useSmartCache()
141152 */
142- protected AssetKey key;
143-
153+ protected AssetKey key;
144154 /**
145155 * Spatial's parent, or null if it has none.
146156 */
@@ -177,14 +187,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
177187 this.name = name;
178188 }
179189
180- public void setKey(AssetKey key){
190+ public void setKey(AssetKey key) {
181191 this.key = key;
182192 }
183-
184- public AssetKey getKey(){
193+
194+ public AssetKey getKey() {
185195 return key;
186196 }
187-
197+
188198 /**
189199 * Indicate that the transform of this spatial has changed and that
190200 * a refresh is required.
@@ -205,7 +215,6 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
205215 protected void setBoundRefresh() {
206216 refreshFlags |= RF_BOUND;
207217
208- // XXX: Replace with a recursive call?
209218 Spatial p = parent;
210219 while (p != null) {
211220 if ((p.refreshFlags & RF_BOUND) != 0) {
@@ -216,7 +225,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
216225 p = p.parent;
217226 }
218227 }
219-
228+
220229 /**
221230 * (Internal use only) Forces a refresh of the given types of data.
222231 *
@@ -401,11 +410,16 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
401410
402411 /**
403412 * <code>lookAt</code> is a convenience method for auto-setting the local
404- * rotation based on a position and an up vector. It computes the rotation
413+ * rotation based on a position in world space and an up vector. It computes the rotation
405414 * to transform the z-axis to point onto 'position' and the y-axis to 'up'.
406415 * Unlike {@link Quaternion#lookAt(com.jme3.math.Vector3f, com.jme3.math.Vector3f) }
407416 * this method takes a world position to look at and not a relative direction.
408417 *
418+ * Note : 28/01/2013 this method has been fixed as it was not taking into account the parent rotation.
419+ * This was resulting in improper rotation when the spatial had rotated parent nodes.
420+ * This method is intended to work in world space, so no matter what parent graph the
421+ * spatial has, it will look at the given position in world space.
422+ *
409423 * @param position
410424 * where to look at in terms of world coordinates
411425 * @param upVector
@@ -418,11 +432,17 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
418432 TempVars vars = TempVars.get();
419433
420434 Vector3f compVecA = vars.vect4;
421- vars.release();
422-
435+
423436 compVecA.set(position).subtractLocal(worldTranslation);
424- getLocalRotation().lookAt(compVecA, upVector);
425-
437+ getLocalRotation().lookAt(compVecA, upVector);
438+
439+ if ( getParent() != null ) {
440+ Quaternion rot=vars.quat1;
441+ rot = rot.set(parent.getWorldRotation()).inverseLocal().multLocal(getLocalRotation());
442+ rot.normalizeLocal();
443+ setLocalRotation(rot);
444+ }
445+ vars.release();
426446 setTransformRefresh();
427447 }
428448
@@ -567,7 +587,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
567587 return;
568588 }
569589
570- for (Control c : controls.getArray() ) {
590+ for (Control c : controls.getArray()) {
571591 c.render(rm, vp);
572592 }
573593 }
@@ -627,7 +647,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
627647 public <T extends Control> T getControl(Class<T> controlType) {
628648 for (Control c : controls.getArray()) {
629649 if (controlType.isAssignableFrom(c.getClass())) {
630- return (T)c;
650+ return (T) c;
631651 }
632652 }
633653 return null;
@@ -994,15 +1014,15 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
9941014 }
9951015
9961016 /**
997- * Rotates the spatial by the yaw, roll and pitch angles (in radians),
998- * in the local coordinate space.
1017+ * Rotates the spatial by the xAngle, yAngle and zAngle angles (in radians),
1018+ * (aka pitch, yaw, roll) in the local coordinate space.
9991019 *
10001020 * @return The spatial on which this method is called, e.g <code>this</code>.
10011021 */
1002- public Spatial rotate(float yaw, float roll, float pitch) {
1022+ public Spatial rotate(float xAngle, float yAngle, float zAngle) {
10031023 TempVars vars = TempVars.get();
10041024 Quaternion q = vars.quat1;
1005- q.fromAngles(yaw, roll, pitch);
1025+ q.fromAngles(xAngle, yAngle, zAngle);
10061026 rotate(q);
10071027 vars.release();
10081028
@@ -1038,6 +1058,16 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
10381058 }
10391059 }
10401060
1061+ public BatchHint getBatchHint() {
1062+ if (batchHint != BatchHint.Inherit) {
1063+ return batchHint;
1064+ } else if (parent != null) {
1065+ return parent.getBatchHint();
1066+ } else {
1067+ return BatchHint.Always;
1068+ }
1069+ }
1070+
10411071 /**
10421072 * Returns this spatial's renderqueue bucket. If the mode is set to inherit,
10431073 * then the spatial gets its renderqueue bucket from its parent.
@@ -1153,7 +1183,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
11531183
11541184 clone.controls = new SafeArrayList<Control>(Control.class);
11551185 for (int i = 0; i < controls.size(); i++) {
1156- clone.controls.add(controls.get(i).cloneForSpatial(clone));
1186+ Control newControl = controls.get(i).cloneForSpatial(clone);
1187+ newControl.setSpatial(clone);
1188+ clone.controls.add(newControl);
11571189 }
11581190
11591191 if (userData != null) {
@@ -1197,7 +1229,9 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
11971229 userData = new HashMap<String, Savable>();
11981230 }
11991231
1200- if (data instanceof Savable) {
1232+ if(data == null){
1233+ userData.remove(key);
1234+ }else if (data instanceof Savable) {
12011235 userData.put(key, (Savable) data);
12021236 } else {
12031237 userData.put(key, new UserData(UserData.getObjectType(data), data));
@@ -1260,13 +1294,14 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
12601294 capsule.write(name, "name", null);
12611295 capsule.write(worldBound, "world_bound", null);
12621296 capsule.write(cullHint, "cull_mode", CullHint.Inherit);
1297+ capsule.write(batchHint, "batch_hint", BatchHint.Inherit);
12631298 capsule.write(queueBucket, "queue", RenderQueue.Bucket.Inherit);
12641299 capsule.write(shadowMode, "shadow_mode", ShadowMode.Inherit);
12651300 capsule.write(localTransform, "transform", Transform.IDENTITY);
12661301 capsule.write(localLights, "lights", null);
1267-
1302+
12681303 // Shallow clone the controls array to convert its type.
1269- capsule.writeSavableArrayList( new ArrayList(controls), "controlsList", null);
1304+ capsule.writeSavableArrayList(new ArrayList(controls), "controlsList", null);
12701305 capsule.writeStringSavableMap(userData, "user_data", null);
12711306 }
12721307
@@ -1276,6 +1311,7 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
12761311 name = ic.readString("name", null);
12771312 worldBound = (BoundingVolume) ic.readSavable("world_bound", null);
12781313 cullHint = ic.readEnum("cull_mode", CullHint.class, CullHint.Inherit);
1314+ batchHint = ic.readEnum("batch_hint", BatchHint.class, BatchHint.Inherit);
12791315 queueBucket = ic.readEnum("queue", RenderQueue.Bucket.class,
12801316 RenderQueue.Bucket.Inherit);
12811317 shadowMode = ic.readEnum("shadow_mode", ShadowMode.class,
@@ -1321,6 +1357,18 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
13211357 }
13221358
13231359 /**
1360+ * <code>setBatchHint</code> sets how batching should work on this
1361+ * spatial. NOTE: You must set this AFTER attaching to a
1362+ * parent or it will be reset with the parent's cullMode value.
1363+ *
1364+ * @param hint
1365+ * one of BatchHint.Never, BatchHint.Always, BatchHint.Inherit
1366+ */
1367+ public void setBatchHint(BatchHint hint) {
1368+ batchHint = hint;
1369+ }
1370+
1371+ /**
13241372 * @return the cullmode set on this Spatial
13251373 */
13261374 public CullHint getLocalCullHint() {
@@ -1328,6 +1376,13 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Asset {
13281376 }
13291377
13301378 /**
1379+ * @return the batchHint set on this Spatial
1380+ */
1381+ public BatchHint getLocalBatchHint() {
1382+ return batchHint;
1383+ }
1384+
1385+ /**
13311386 * <code>setQueueBucket</code> determines at what phase of the
13321387 * rendering process this Spatial will rendered. See the
13331388 * {@link Bucket} enum for an explanation of the various
--- a/engine/src/core/com/jme3/scene/VertexBuffer.java
+++ b/engine/src/core/com/jme3/scene/VertexBuffer.java
@@ -743,6 +743,22 @@ public class VertexBuffer extends NativeObject implements Savable, Cloneable {
743743 * match.
744744 */
745745 public void copyElement(int inIndex, VertexBuffer outVb, int outIndex){
746+ copyElements(inIndex, outVb, outIndex, 1);
747+ }
748+
749+ /**
750+ * Copies a sequence of elements of data from this <code>VertexBuffer</code>
751+ * to the given output VertexBuffer.
752+ *
753+ * @param inIndex The input element index
754+ * @param outVb The buffer to copy to
755+ * @param outIndex The output element index
756+ * @param len The number of elements to copy
757+ *
758+ * @throws IllegalArgumentException If the formats of the buffers do not
759+ * match.
760+ */
761+ public void copyElements(int inIndex, VertexBuffer outVb, int outIndex, int len){
746762 if (outVb.format != format || outVb.components != components)
747763 throw new IllegalArgumentException("Buffer format mismatch. Cannot copy");
748764