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 33 package com.jme3.scene.plugins.ogre; 34 35 import com.jme3.asset.*; 36 import com.jme3.light.DirectionalLight; 37 import com.jme3.light.Light; 38 import com.jme3.light.PointLight; 39 import com.jme3.light.SpotLight; 40 import com.jme3.material.MaterialList; 41 import com.jme3.math.Quaternion; 42 import com.jme3.math.Vector3f; 43 import com.jme3.scene.Spatial; 44 import com.jme3.scene.plugins.ogre.matext.OgreMaterialKey; 45 import com.jme3.util.PlaceholderAssets; 46 import com.jme3.util.xml.SAXUtil; 47 import static com.jme3.util.xml.SAXUtil.*; 48 import java.io.IOException; 49 import java.io.InputStreamReader; 50 import java.util.Stack; 51 import java.util.logging.Level; 52 import java.util.logging.Logger; 53 import javax.xml.parsers.ParserConfigurationException; 54 import javax.xml.parsers.SAXParserFactory; 55 import org.xml.sax.Attributes; 56 import org.xml.sax.InputSource; 57 import org.xml.sax.SAXException; 58 import org.xml.sax.XMLReader; 59 import org.xml.sax.helpers.DefaultHandler; 60 61 public 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