/**
* Copyright (c) 2008-2012 Ardor Labs, Inc.
*
* This file is part of Ardor3D.
*
* Ardor3D is free software: you can redistribute it and/or modify it
* under the terms of its license which may be found in the accompanying
* LICENSE file or at <http://www.ardor3d.com/LICENSE>.
*/
package com.ardor3d.extension.model.collada.jdom;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;
import org.jdom2.Element;
import com.ardor3d.extension.model.collada.jdom.data.DataCache;
import com.ardor3d.extension.model.collada.jdom.data.MaterialInfo;
import com.ardor3d.extension.model.collada.jdom.data.SamplerTypes;
import com.ardor3d.image.Texture;
import com.ardor3d.image.Texture.ApplyMode;
import com.ardor3d.image.Texture.CombinerFunctionRGB;
import com.ardor3d.image.Texture.CombinerOperandRGB;
import com.ardor3d.image.Texture.CombinerSource;
import com.ardor3d.image.TextureStoreFormat;
import com.ardor3d.math.ColorRGBA;
import com.ardor3d.math.MathUtils;
import com.ardor3d.renderer.queue.RenderBucketType;
import com.ardor3d.renderer.state.BlendState;
import com.ardor3d.renderer.state.MaterialState;
import com.ardor3d.renderer.state.MaterialState.MaterialFace;
import com.ardor3d.renderer.state.RenderState;
import com.ardor3d.renderer.state.TextureState;
import com.ardor3d.scenegraph.Mesh;
import com.ardor3d.util.TextureManager;
import com.ardor3d.util.resource.ResourceSource;
/**
* Methods for parsing Collada data related to materials.
*/
public class ColladaMaterialUtils {
private static final Logger logger = Logger.getLogger(ColladaMaterialUtils.class.getName());
private final ColladaImporter _importer;
private final DataCache _dataCache;
private final ColladaDOMUtil _colladaDOMUtil;
private final boolean _compressTextures, _loadTextures, _flipTransparency;
public ColladaMaterialUtils(final ColladaImporter importer, final DataCache dataCache,
final ColladaDOMUtil colladaDOMUtil) {
_importer = importer;
_dataCache = dataCache;
_colladaDOMUtil = colladaDOMUtil;
_compressTextures = _importer.isCompressTextures();
_loadTextures = _importer.isLoadTextures();
_flipTransparency = _importer.isFlipTransparency();
}
/**
* Find and apply the given material to the given Mesh.
*
* @param materialName
* our material name
* @param mesh
* the mesh to apply material to.
*/
public void applyMaterial(final String materialName, final Mesh mesh) {
if (materialName == null) {
logger.warning("materialName is null");
return;
}
Element mat = _dataCache.getBoundMaterial(materialName);
if (mat == null) {
logger.warning("material not bound: " + materialName + ", trying search with id.");
mat = _colladaDOMUtil.findTargetWithId(materialName);
}
if (mat == null || !"material".equals(mat.getName())) {
logger.warning("material not found: " + materialName);
return;
}
final String originalMaterial = mat.getAttributeValue("id");
MaterialInfo mInfo = null;
if (!_dataCache.getMaterialInfoMap().containsKey(originalMaterial)) {
mInfo = new MaterialInfo();
mInfo.setMaterialName(originalMaterial);
_dataCache.getMaterialInfoMap().put(originalMaterial, mInfo);
}
_dataCache.getMeshMaterialMap().put(mesh, originalMaterial);
final Element child = mat.getChild("instance_effect");
final Element effectNode = _colladaDOMUtil.findTargetWithId(child.getAttributeValue("url"));
if (effectNode == null) {
logger.warning("material effect not found: " + mat.getChild("instance_material").getAttributeValue("url"));
return;
}
if ("effect".equals(effectNode.getName())) {
/*
* temp cache for textures, we do not want to add textures twice (for example, transparant map might point
* to diffuse texture)
*/
final HashMap<String, Texture> loadedTextures = new HashMap<String, Texture>();
final Element effect = effectNode;
// XXX: For now, just grab the common technique:
final Element common = effect.getChild("profile_COMMON");
if (common != null) {
if (mInfo != null) {
mInfo.setProfile("COMMON");
}
final Element commonExtra = common.getChild("extra");
if (commonExtra != null) {
// process with any plugins
_importer.readExtra(commonExtra, mesh);
}
final Element technique = common.getChild("technique");
String type = "blinn";
if (technique.getChild(type) == null) {
type = "phong";
if (technique.getChild(type) == null) {
type = "lambert";
if (technique.getChild(type) == null) {
type = "constant";
if (technique.getChild(type) == null) {
ColladaMaterialUtils.logger.warning("COMMON material has unusuable techinque. "
+ child.getAttributeValue("url"));
return;
}
}
}
}
final Element blinnPhongLambert = technique.getChild(type);
if (mInfo != null) {
mInfo.setTechnique(type);
}
final MaterialState mState = new MaterialState();
// TODO: implement proper transparency handling
Texture diffuseTexture = null;
ColorRGBA transparent = new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
float transparency = 1.0f;
boolean useTransparency = false;
String opaqueMode = "A_ONE";
/*
* place holder for current property, we import material properties in fixed order (for texture order)
*/
Element property = null;
/* Diffuse property */
property = blinnPhongLambert.getChild("diffuse");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("color".equals(propertyValue.getName())) {
final ColorRGBA color = _colladaDOMUtil.getColor(propertyValue.getText());
mState.setDiffuse(MaterialFace.FrontAndBack, color);
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
diffuseTexture = populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo,
"diffuse");
}
}
/* Ambient property */
property = blinnPhongLambert.getChild("ambient");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("color".equals(propertyValue.getName())) {
final ColorRGBA color = _colladaDOMUtil.getColor(propertyValue.getText());
mState.setAmbient(MaterialFace.FrontAndBack, color);
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "ambient");
}
}
/* Transparent property */
property = blinnPhongLambert.getChild("transparent");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("color".equals(propertyValue.getName())) {
transparent = _colladaDOMUtil.getColor(propertyValue.getText());
// TODO: use this
useTransparency = true;
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "transparent");
}
opaqueMode = property.getAttributeValue("opaque", "A_ONE");
}
/* Transparency property */
property = blinnPhongLambert.getChild("transparency");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("float".equals(propertyValue.getName())) {
transparency = Float.parseFloat(propertyValue.getText().replace(",", "."));
// TODO: use this
if (_flipTransparency) {
transparency = 1f - transparency;
}
useTransparency = true;
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "transparency");
}
}
/* Emission property */
property = blinnPhongLambert.getChild("emission");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("color".equals(propertyValue.getName())) {
mState.setEmissive(MaterialFace.FrontAndBack, _colladaDOMUtil.getColor(propertyValue.getText()));
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "emissive");
}
}
/* Specular property */
property = blinnPhongLambert.getChild("specular");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("color".equals(propertyValue.getName())) {
mState.setSpecular(MaterialFace.FrontAndBack, _colladaDOMUtil.getColor(propertyValue.getText()));
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "specular");
}
}
/* Shininess property */
property = blinnPhongLambert.getChild("shininess");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("float".equals(propertyValue.getName())) {
float shininess = Float.parseFloat(propertyValue.getText().replace(",", "."));
if (shininess >= 0.0f && shininess <= 1.0f) {
final float oldShininess = shininess;
shininess *= 128;
logger.finest("Shininess - " + oldShininess
+ " - was in the [0,1] range. Scaling to [0, 128] - " + shininess);
} else if (shininess < 0 || shininess > 128) {
final float oldShininess = shininess;
shininess = MathUtils.clamp(shininess, 0, 128);
logger.warning("Shininess must be between 0 and 128. Shininess " + oldShininess
+ " was clamped to " + shininess);
}
mState.setShininess(MaterialFace.FrontAndBack, shininess);
} else if ("texture".equals(propertyValue.getName()) && _loadTextures) {
populateTextureState(mesh, propertyValue, effect, loadedTextures, mInfo, "shininess");
}
}
/* Reflectivity property */
float reflectivity = 1.0f;
property = blinnPhongLambert.getChild("reflectivity");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("float".equals(propertyValue.getName())) {
reflectivity = Float.parseFloat(propertyValue.getText().replace(",", "."));
}
}
/* Reflective property. Texture only */
property = blinnPhongLambert.getChild("reflective");
if (property != null) {
final Element propertyValue = property.getChildren().get(0);
if ("texture".equals(propertyValue.getName()) && _loadTextures) {
final Texture reflectiveTexture = populateTextureState(mesh, propertyValue, effect,
loadedTextures, mInfo, "reflective");
reflectiveTexture.setEnvironmentalMapMode(Texture.EnvironmentalMapMode.SphereMap);
reflectiveTexture.setApply(ApplyMode.Combine);
reflectiveTexture.setCombineFuncRGB(CombinerFunctionRGB.Interpolate);
// color 1
reflectiveTexture.setCombineSrc0RGB(CombinerSource.CurrentTexture);
reflectiveTexture.setCombineOp0RGB(CombinerOperandRGB.SourceColor);
// color 2
reflectiveTexture.setCombineSrc1RGB(CombinerSource.Previous);
reflectiveTexture.setCombineOp1RGB(CombinerOperandRGB.SourceColor);
// interpolate param will come from alpha of constant color
reflectiveTexture.setCombineSrc2RGB(CombinerSource.Constant);
reflectiveTexture.setCombineOp2RGB(CombinerOperandRGB.SourceAlpha);
reflectiveTexture.setConstantColor(new ColorRGBA(1, 1, 1, reflectivity));
}
}
/*
* An extra tag defines some materials not part of the collada standard. Since we're not able to parse
* we simply extract the textures from the element (such that shaders etc can at least pick up on them)
*/
property = technique.getChild("extra");
if (property != null) {
// process with any plugins
if (!_importer.readExtra(property, mesh)) {
// no plugin processed our mesh, so process ourselves.
getTexturesFromElement(mesh, property, effect, loadedTextures, mInfo);
}
}
// XXX: There are some issues with clarity on how to use alpha blending in OpenGL FFP.
// The best interpretation I have seen is that if transparent has a texture == diffuse,
// Turn on alpha blending and use diffuse alpha.
// check to make sure we actually need this.
// testing separately for a transparency of 0.0 is to hack around erroneous exports, since usually
// there is no use in exporting something with 100% transparency.
if ("A_ONE".equals(opaqueMode) && ColorRGBA.WHITE.equals(transparent) && transparency == 1.0
|| transparency == 0.0) {
useTransparency = false;
}
if (useTransparency) {
if (diffuseTexture != null) {
final BlendState blend = new BlendState();
blend.setBlendEnabled(true);
blend.setTestEnabled(true);
blend.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
mesh.setRenderState(blend);
} else {
final BlendState blend = new BlendState();
blend.setBlendEnabled(true);
blend.setTestEnabled(true);
transparent.setAlpha(transparent.getAlpha() * transparency);
blend.setConstantColor(transparent);
blend.setSourceFunction(BlendState.SourceFunction.ConstantAlpha);
blend.setDestinationFunction(BlendState.DestinationFunction.OneMinusConstantAlpha);
mesh.setRenderState(blend);
}
mesh.getSceneHints().setRenderBucketType(RenderBucketType.Transparent);
}
if (mInfo != null) {
if (useTransparency) {
mInfo.setUseTransparency(useTransparency);
if (diffuseTexture == null) {
mInfo.setTransparency(transparent.getAlpha() * transparency);
}
}
mInfo.setMaterialState(mState);
}
mesh.setRenderState(mState);
}
} else {
ColladaMaterialUtils.logger.warning("material effect not found: "
+ mat.getChild("instance_material").getAttributeValue("url"));
}
}
/**
* Function to searches an xml node for <texture> elements and adds them to the texture state of the mesh.
*
* @param mesh
* the Ardor3D Mesh to add the Texture to.
* @param element
* the xml element to start the search on
* @param effect
* our <instance_effect> element
* @param info
*/
private void getTexturesFromElement(final Mesh mesh, final Element element, final Element effect,
final HashMap<String, Texture> loadedTextures, final MaterialInfo info) {
if ("texture".equals(element.getName()) && _loadTextures) {
populateTextureState(mesh, element, effect, loadedTextures, info, null);
}
@SuppressWarnings("unchecked")
final List<Element> children = element.getChildren();
if (children != null) {
for (final Element child : children) {
/* recurse on children */
getTexturesFromElement(mesh, child, effect, loadedTextures, info);
}
}
}
/**
* Convert a <texture> element to an Ardor3D representation and store in the given state.
*
* @param mesh
* the Ardor3D Mesh to add the Texture to.
* @param daeTexture
* our <texture> element
* @param effect
* our <instance_effect> element
* @return the created Texture.
*/
private Texture populateTextureState(final Mesh mesh, final Element daeTexture, final Element effect,
final HashMap<String, Texture> loadedTextures, final MaterialInfo info, String textureSlot) {
// TODO: Use vert data to determine which texcoords and set to use.
// final String uvName = daeTexture.getAttributeValue("texcoord");
TextureState tState = (TextureState) mesh.getLocalRenderState(RenderState.StateType.Texture);
if (tState == null) {
tState = new TextureState();
mesh.setRenderState(tState);
}
// Use texture attrib to find correct sampler
final String textureReference = daeTexture.getAttributeValue("texture");
if (textureSlot == null) {
// if we have no texture slot defined (like in the case of an "extra" texture), we'll use the
// textureReference.
textureSlot = textureReference;
}
/* only add the texture to the state once */
if (loadedTextures.containsKey(textureReference)) {
final Texture tex = loadedTextures.get(textureReference);
if (info != null) {
info.setTextureSlot(textureSlot, textureReference, tex, null);
}
return tex;
}
Element node = _colladaDOMUtil.findTargetWithSid(textureReference);
if (node == null) {
// Not sure if this is quite right, but spec seems to indicate looking for global id
node = _colladaDOMUtil.findTargetWithId("#" + textureReference);
}
if ("newparam".equals(node.getName())) {
node = node.getChildren().get(0);
}
Element sampler = null;
Element surface = null;
Element image = null;
Texture.MinificationFilter min = Texture.MinificationFilter.BilinearNoMipMaps;
if ("sampler2D".equals(node.getName())) {
sampler = node;
if (sampler.getChild("minfilter") != null) {
final String minfilter = sampler.getChild("minfilter").getText();
min = Enum.valueOf(SamplerTypes.MinFilterType.class, minfilter).getArdor3dFilter();
}
// Use sampler to get correct surface
node = _colladaDOMUtil.findTargetWithSid(sampler.getChild("source").getText());
// node = resolveSid(effect, sampler.getSource());
}
if ("newparam".equals(node.getName())) {
node = node.getChildren().get(0);
}
if ("surface".equals(node.getName())) {
surface = node;
// image(s) will come from surface.
} else if ("image".equals(node.getName())) {
image = node;
}
// Ok, a few possibilities here...
Texture texture = null;
String fileName = null;
if (surface == null && image != null) {
// Only an image found (no sampler). Assume 2d texture. Load.
fileName = image.getChild("init_from").getText();
texture = loadTexture2D(fileName, min);
} else if (surface != null) {
// We have a surface, pull images from that.
if ("2D".equals(surface.getAttributeValue("type"))) {
// look for an init_from with lowest mip and use that. (usually 0)
// TODO: mip?
final Element lowest = surface.getChildren("init_from").get(0);
// Element lowest = null;
// for (final Element i : (List<Element>) surface.getChildren("init_from")) {
// if (lowest == null || lowest.getMip() > i.getMip()) {
// lowest = i;
// }
// }
if (lowest == null) {
logger.warning("surface given with no usable init_from: " + surface);
return null;
}
image = _colladaDOMUtil.findTargetWithId("#" + lowest.getText());
// image = (DaeImage) root.resolveUrl("#" + lowest.getValue());
if (image != null) {
fileName = image.getChild("init_from").getText();
texture = loadTexture2D(fileName, min);
}
// TODO: add support for mip map levels other than 0.
}
// TODO: add support for the other texture types.
} else {
// No surface OR image... warn.
logger.warning("texture given with no matching <sampler*> or <image> found.");
if (info != null) {
info.setTextureSlot(textureSlot, textureReference, null, null);
}
return null;
}
if (texture != null) {
if (sampler != null) {
// Apply params from our sampler.
applySampler(sampler, texture);
}
// Add to texture state.
tState.setTexture(texture, tState.getNumberOfSetTextures());
loadedTextures.put(textureReference, texture);
if (info != null) {
info.setTextureSlot(textureSlot, textureReference, texture, fileName);
}
} else {
logger.warning("unable to load texture: " + daeTexture);
if (info != null) {
info.setTextureSlot(textureSlot, textureReference, null, fileName);
}
}
return texture;
}
private void applySampler(final Element sampler, final Texture texture) {
if (sampler.getChild("minfilter") != null) {
final String minfilter = sampler.getChild("minfilter").getText();
texture.setMinificationFilter(Enum.valueOf(SamplerTypes.MinFilterType.class, minfilter).getArdor3dFilter());
}
if (sampler.getChild("magfilter") != null) {
final String magfilter = sampler.getChild("magfilter").getText();
texture.setMagnificationFilter(Enum.valueOf(SamplerTypes.MagFilterType.class, magfilter).getArdor3dFilter());
}
if (sampler.getChild("wrap_s") != null) {
final String wrapS = sampler.getChild("wrap_s").getText();
texture.setWrap(Texture.WrapAxis.S, Enum.valueOf(SamplerTypes.WrapModeType.class, wrapS)
.getArdor3dWrapMode());
}
if (sampler.getChild("wrap_t") != null) {
final String wrapT = sampler.getChild("wrap_t").getText();
texture.setWrap(Texture.WrapAxis.T, Enum.valueOf(SamplerTypes.WrapModeType.class, wrapT)
.getArdor3dWrapMode());
}
if (sampler.getChild("border_color") != null) {
texture.setBorderColor(_colladaDOMUtil.getColor(sampler.getChild("border_color").getText()));
}
}
@SuppressWarnings("unchecked")
public void bindMaterials(final Element bindMaterial) {
if (bindMaterial == null || bindMaterial.getChildren().isEmpty()) {
return;
}
for (final Element instance : bindMaterial.getChild("technique_common").getChildren("instance_material")) {
final Element matNode = _colladaDOMUtil.findTargetWithId(instance.getAttributeValue("target"));
if (matNode != null && "material".equals(matNode.getName())) {
_dataCache.bindMaterial(instance.getAttributeValue("symbol"), matNode);
} else {
logger.warning("instance material target not found: " + instance.getAttributeValue("target"));
}
// TODO: need to store bound vert data as local data. (also unstore on unbind.)
}
}
@SuppressWarnings("unchecked")
public void unbindMaterials(final Element bindMaterial) {
if (bindMaterial == null || bindMaterial.getChildren().isEmpty()) {
return;
}
for (final Element instance : bindMaterial.getChild("technique_common").getChildren("instance_material")) {
_dataCache.unbindMaterial(instance.getAttributeValue("symbol"));
}
}
private Texture loadTexture2D(final String path, final Texture.MinificationFilter minFilter) {
if (_dataCache.containsTexture(path)) {
return _dataCache.getTexture(path);
}
final Texture texture;
if (_importer.getTextureLocator() == null) {
texture = TextureManager.load(path, minFilter, _compressTextures ? TextureStoreFormat.GuessCompressedFormat
: TextureStoreFormat.GuessNoCompressedFormat, true);
} else {
final ResourceSource source = _importer.getTextureLocator().locateResource(path);
texture = TextureManager.load(source, minFilter,
_compressTextures ? TextureStoreFormat.GuessCompressedFormat
: TextureStoreFormat.GuessNoCompressedFormat, true);
}
_dataCache.addTexture(path, texture);
return texture;
}
}