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 */
32
33package com.jme3.scene.plugins.ogre;
34
35import com.jme3.asset.*;
36import com.jme3.light.DirectionalLight;
37import com.jme3.light.Light;
38import com.jme3.light.PointLight;
39import com.jme3.light.SpotLight;
40import com.jme3.material.MaterialList;
41import com.jme3.math.Quaternion;
42import com.jme3.math.Vector3f;
43import com.jme3.scene.Spatial;
44import com.jme3.scene.plugins.ogre.matext.OgreMaterialKey;
45import com.jme3.util.PlaceholderAssets;
46import com.jme3.util.xml.SAXUtil;
47import static com.jme3.util.xml.SAXUtil.*;
48import java.io.IOException;
49import java.io.InputStreamReader;
50import java.util.Stack;
51import java.util.logging.Level;
52import java.util.logging.Logger;
53import javax.xml.parsers.ParserConfigurationException;
54import javax.xml.parsers.SAXParserFactory;
55import org.xml.sax.Attributes;
56import org.xml.sax.InputSource;
57import org.xml.sax.SAXException;
58import org.xml.sax.XMLReader;
59import org.xml.sax.helpers.DefaultHandler;
60
61public class SceneLoader extends DefaultHandler implements AssetLoader {
62
63    private static final Logger logger = Logger.getLogger(SceneLoader.class.getName());
64
65    private Stack<String> elementStack = new Stack<String>();
66    private AssetKey key;
67    private String sceneName;
68    private String folderName;
69    private AssetManager assetManager;
70    private MaterialList materialList;
71    private com.jme3.scene.Node root;
72    private com.jme3.scene.Node node;
73    private com.jme3.scene.Node entityNode;
74    private Light light;
75    private int nodeIdx = 0;
76    private static volatile int sceneIdx = 0;
77
78    public SceneLoader(){
79        super();
80    }
81
82    @Override
83    public void startDocument() {
84    }
85
86    @Override
87    public void endDocument() {
88    }
89
90    private void reset(){
91        elementStack.clear();
92        nodeIdx = 0;
93
94        // NOTE: Setting some of those to null is only needed
95        // if the parsed file had an error e.g. startElement was called
96        // but not endElement
97        root = null;
98        node = null;
99        entityNode = null;
100        light = null;
101    }
102
103    private void checkTopNode(String topNode) throws SAXException{
104        if (!elementStack.peek().equals(topNode)){
105            throw new SAXException("dotScene parse error: Expected parent node to be " + topNode);
106        }
107    }
108
109    private Quaternion parseQuat(Attributes attribs) throws SAXException{
110        if (attribs.getValue("x") != null){
111            // defined as quaternion
112            float x = parseFloat(attribs.getValue("x"));
113            float y = parseFloat(attribs.getValue("y"));
114            float z = parseFloat(attribs.getValue("z"));
115            float w = parseFloat(attribs.getValue("w"));
116            return new Quaternion(x,y,z,w);
117        }else if (attribs.getValue("qx") != null){
118            // defined as quaternion with prefix "q"
119            float x = parseFloat(attribs.getValue("qx"));
120            float y = parseFloat(attribs.getValue("qy"));
121            float z = parseFloat(attribs.getValue("qz"));
122            float w = parseFloat(attribs.getValue("qw"));
123            return new Quaternion(x,y,z,w);
124        }else if (attribs.getValue("angle") != null){
125            // defined as angle + axis
126            float angle = parseFloat(attribs.getValue("angle"));
127            float axisX = parseFloat(attribs.getValue("axisX"));
128            float axisY = parseFloat(attribs.getValue("axisY"));
129            float axisZ = parseFloat(attribs.getValue("axisZ"));
130            Quaternion q = new Quaternion();
131            q.fromAngleAxis(angle, new Vector3f(axisX, axisY, axisZ));
132            return q;
133        }else{
134            // defines as 3 angles along XYZ axes
135            float angleX = parseFloat(attribs.getValue("angleX"));
136            float angleY = parseFloat(attribs.getValue("angleY"));
137            float angleZ = parseFloat(attribs.getValue("angleZ"));
138            Quaternion q = new Quaternion();
139            q.fromAngles(angleX, angleY, angleZ);
140            return q;
141        }
142    }
143
144    private void parseLightNormal(Attributes attribs) throws SAXException {
145        checkTopNode("light");
146
147        // SpotLight will be supporting a direction-normal, too.
148        if (light instanceof DirectionalLight)
149            ((DirectionalLight) light).setDirection(parseVector3(attribs));
150        else if (light instanceof SpotLight){
151            ((SpotLight) light).setDirection(parseVector3(attribs));
152        }
153    }
154
155    private void parseLightAttenuation(Attributes attribs) throws SAXException {
156        // NOTE: Derives range based on "linear" if it is used solely
157        // for the attenuation. Otherwise derives it from "range"
158        checkTopNode("light");
159
160        if (light instanceof PointLight || light instanceof SpotLight){
161            float range = parseFloat(attribs.getValue("range"));
162            float constant = parseFloat(attribs.getValue("constant"));
163            float linear = parseFloat(attribs.getValue("linear"));
164
165            String quadraticStr = attribs.getValue("quadratic");
166            if (quadraticStr == null)
167                quadraticStr = attribs.getValue("quadric");
168
169            float quadratic = parseFloat(quadraticStr);
170
171            if (constant == 1 && quadratic == 0 && linear > 0){
172                range = 1f / linear;
173            }
174
175            if (light instanceof PointLight){
176                ((PointLight) light).setRadius(range);
177            }else{
178                ((SpotLight)light).setSpotRange(range);
179            }
180        }
181    }
182
183    private void parseLightSpotLightRange(Attributes attribs) throws SAXException{
184        checkTopNode("light");
185
186        float outer = SAXUtil.parseFloat(attribs.getValue("outer"));
187        float inner = SAXUtil.parseFloat(attribs.getValue("inner"));
188
189        if (!(light instanceof SpotLight)){
190            throw new SAXException("dotScene parse error: spotLightRange "
191                    + "can only appear under 'spot' light elements");
192        }
193
194        SpotLight sl = (SpotLight) light;
195        sl.setSpotInnerAngle(inner * 0.5f);
196        sl.setSpotOuterAngle(outer * 0.5f);
197    }
198
199    private void parseLight(Attributes attribs) throws SAXException {
200        if (node == null || node.getParent() == null)
201            throw new SAXException("dotScene parse error: light can only appear under a node");
202
203        checkTopNode("node");
204
205        String lightType = parseString(attribs.getValue("type"), "point");
206        if(lightType.equals("point")) {
207            light = new PointLight();
208        } else if(lightType.equals("directional") || lightType.equals("sun")) {
209            light = new DirectionalLight();
210            // Assuming "normal" property is not provided
211            ((DirectionalLight)light).setDirection(Vector3f.UNIT_Z);
212        } else if(lightType.equals("spotLight") || lightType.equals("spot")) {
213            light = new SpotLight();
214        } else {
215            logger.log(Level.WARNING, "No matching jME3 LightType found for OGRE LightType: {0}", lightType);
216        }
217        logger.log(Level.FINEST, "{0} created.", light);
218
219        if (!parseBool(attribs.getValue("visible"), true)){
220            // set to disabled
221        }
222
223        // "attach" it to the parent of this node
224        if (light != null)
225            node.getParent().addLight(light);
226    }
227
228    @Override
229    public void startElement(String uri, String localName, String qName, Attributes attribs) throws SAXException{
230        if (qName.equals("scene")){
231            if (elementStack.size() != 0){
232                throw new SAXException("dotScene parse error: 'scene' element must be the root XML element");
233            }
234
235            String version = attribs.getValue("formatVersion");
236            if (version == null || (!version.equals("1.0.0") && !version.equals("1.0.1")))
237                logger.log(Level.WARNING, "Unrecognized version number"
238                        + " in dotScene file: {0}", version);
239
240        }else if (qName.equals("nodes")){
241            if (root != null){
242                throw new SAXException("dotScene parse error: nodes element was specified twice");
243            }
244            if (sceneName == null)
245                root = new com.jme3.scene.Node("OgreDotScene"+(++sceneIdx));
246            else
247                root = new com.jme3.scene.Node(sceneName+"-scene_node");
248
249            node = root;
250        }else if (qName.equals("externals")){
251            checkTopNode("scene");
252            // Not loaded currently
253        }else if (qName.equals("item")){
254            checkTopNode("externals");
255        }else if (qName.equals("file")){
256            checkTopNode("item");
257
258            // XXX: Currently material file name is based
259            // on the scene's filename. THIS IS NOT CORRECT.
260            // To solve, port SceneLoader to use DOM instead of SAX
261
262            //String matFile = folderName+attribs.getValue("name");
263            //try {
264            //    materialList = (MaterialList) assetManager.loadAsset(new OgreMaterialKey(matFile));
265            //} catch (AssetNotFoundException ex){
266            //    materialList = null;
267            //    logger.log(Level.WARNING, "Cannot locate material file: {0}", matFile);
268            //}
269        }else if (qName.equals("node")){
270            String curElement = elementStack.peek();
271            if (!curElement.equals("node") && !curElement.equals("nodes")){
272                throw new SAXException("dotScene parse error: "
273                        + "node element can only appear under 'node' or 'nodes'");
274            }
275
276            String name = attribs.getValue("name");
277            if (name == null)
278                name = "OgreNode-" + (++nodeIdx);
279
280            com.jme3.scene.Node newNode = new com.jme3.scene.Node(name);
281            if (node != null){
282                node.attachChild(newNode);
283            }
284            node = newNode;
285        }else if (qName.equals("property")){
286            if (node != null){
287                String type = attribs.getValue("type");
288                String name = attribs.getValue("name");
289                String data = attribs.getValue("data");
290                if (type.equals("BOOL")){
291                    node.setUserData(name, Boolean.parseBoolean(data)||data.equals("1"));
292                }else if (type.equals("FLOAT")){
293                    node.setUserData(name, Float.parseFloat(data));
294                }else if (type.equals("STRING")){
295                    node.setUserData(name, data);
296                }else if (type.equals("INT")){
297                    node.setUserData(name, Integer.parseInt(data));
298                }
299            }
300        }else if (qName.equals("entity")){
301            checkTopNode("node");
302
303            String name = attribs.getValue("name");
304            if (name == null)
305                name = "OgreEntity-" + (++nodeIdx);
306            else
307                name += "-entity";
308
309            String meshFile = attribs.getValue("meshFile");
310            if (meshFile == null) {
311                throw new SAXException("Required attribute 'meshFile' missing for 'entity' node");
312            }
313
314            // TODO: Not currently used
315            String materialName = attribs.getValue("materialName");
316
317            if (folderName != null) {
318                meshFile = folderName + meshFile;
319            }
320
321            // NOTE: append "xml" since its assumed mesh files are binary in dotScene
322            meshFile += ".xml";
323
324            entityNode = new com.jme3.scene.Node(name);
325            OgreMeshKey meshKey = new OgreMeshKey(meshFile, materialList);
326            try {
327                Spatial ogreMesh = assetManager.loadModel(meshKey);
328                entityNode.attachChild(ogreMesh);
329            } catch (AssetNotFoundException ex){
330                logger.log(Level.WARNING, "Cannot locate {0} for scene {1}", new Object[]{meshKey, key});
331                // Attach placeholder asset.
332                entityNode.attachChild(PlaceholderAssets.getPlaceholderModel(assetManager));
333            }
334
335            node.attachChild(entityNode);
336            node = null;
337        }else if (qName.equals("position")){
338            if (elementStack.peek().equals("node")){
339                node.setLocalTranslation(SAXUtil.parseVector3(attribs));
340            }
341        }else if (qName.equals("quaternion") || qName.equals("rotation")){
342            node.setLocalRotation(parseQuat(attribs));
343        }else if (qName.equals("scale")){
344            node.setLocalScale(SAXUtil.parseVector3(attribs));
345        } else if (qName.equals("light")) {
346            parseLight(attribs);
347        } else if (qName.equals("colourDiffuse") || qName.equals("colorDiffuse")) {
348            if (elementStack.peek().equals("light")){
349                if (light != null){
350                    light.setColor(parseColor(attribs));
351                }
352            }else{
353                checkTopNode("environment");
354            }
355        } else if (qName.equals("normal") || qName.equals("direction")) {
356            checkTopNode("light");
357            parseLightNormal(attribs);
358        } else if (qName.equals("lightAttenuation")) {
359            parseLightAttenuation(attribs);
360        } else if (qName.equals("spotLightRange") || qName.equals("lightRange")) {
361            parseLightSpotLightRange(attribs);
362        }
363
364        elementStack.push(qName);
365    }
366
367    @Override
368    public void endElement(String uri, String name, String qName) throws SAXException {
369        if (qName.equals("node")){
370            node = node.getParent();
371        }else if (qName.equals("nodes")){
372            node = null;
373        }else if (qName.equals("entity")){
374            node = entityNode.getParent();
375            entityNode = null;
376        }else if (qName.equals("light")){
377            // apply the node's world transform on the light..
378            root.updateGeometricState();
379            if (light != null){
380                if (light instanceof DirectionalLight){
381                    DirectionalLight dl = (DirectionalLight) light;
382                    Quaternion q = node.getWorldRotation();
383                    Vector3f dir = dl.getDirection();
384                    q.multLocal(dir);
385                    dl.setDirection(dir);
386                }else if (light instanceof PointLight){
387                    PointLight pl = (PointLight) light;
388                    Vector3f pos = node.getWorldTranslation();
389                    pl.setPosition(pos);
390                }else if (light instanceof SpotLight){
391                    SpotLight sl = (SpotLight) light;
392
393                    Vector3f pos = node.getWorldTranslation();
394                    sl.setPosition(pos);
395
396                    Quaternion q = node.getWorldRotation();
397                    Vector3f dir = sl.getDirection();
398                    q.multLocal(dir);
399                    sl.setDirection(dir);
400                }
401            }
402            light = null;
403        }
404        checkTopNode(qName);
405        elementStack.pop();
406    }
407
408    @Override
409    public void characters(char ch[], int start, int length) {
410    }
411
412
413
414    public Object load(AssetInfo info) throws IOException {
415        try{
416            key = info.getKey();
417            assetManager = info.getManager();
418            sceneName = key.getName();
419            String ext = key.getExtension();
420            folderName = key.getFolder();
421            sceneName = sceneName.substring(0, sceneName.length() - ext.length() - 1);
422
423            OgreMaterialKey materialKey = new OgreMaterialKey(sceneName+".material");
424            try {
425                materialList = (MaterialList) assetManager.loadAsset(materialKey);
426            } catch (AssetNotFoundException ex){
427                logger.log(Level.WARNING, "Cannot locate {0} for scene {1}", new Object[]{materialKey, key});
428                materialList = null;
429            }
430
431            reset();
432
433            // Added by larynx 25.06.2011
434            // Android needs the namespace aware flag set to true
435            // Kirill 30.06.2011
436            // Now, hack is applied for both desktop and android to avoid
437            // checking with JmeSystem.
438            SAXParserFactory factory = SAXParserFactory.newInstance();
439            factory.setNamespaceAware(true);
440            XMLReader xr = factory.newSAXParser().getXMLReader();
441
442            xr.setContentHandler(this);
443            xr.setErrorHandler(this);
444
445            InputStreamReader r = null;
446
447            try {
448                r = new InputStreamReader(info.openStream());
449                xr.parse(new InputSource(r));
450            } finally {
451                if (r != null){
452                    r.close();
453                }
454            }
455
456            return root;
457        }catch (SAXException ex){
458            IOException ioEx = new IOException("Error while parsing Ogre3D dotScene");
459            ioEx.initCause(ex);
460            throw ioEx;
461        } catch (ParserConfigurationException ex) {
462            IOException ioEx = new IOException("Error while parsing Ogre3D dotScene");
463            ioEx.initCause(ex);
464            throw ioEx;
465        }
466    }
467
468}
469