/*
* Copyright 2013 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.rendering.md5;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import gnu.trove.list.TIntList;
import gnu.trove.list.array.TIntArrayList;
import org.terasology.assets.ResourceUrn;
import org.terasology.assets.format.AbstractAssetFileFormat;
import org.terasology.assets.format.AssetDataFile;
import org.terasology.assets.module.annotations.RegisterAssetFileFormat;
import org.terasology.math.AABB;
import org.terasology.math.geom.Quat4f;
import org.terasology.math.geom.Vector3f;
import org.terasology.rendering.assets.animation.MeshAnimationData;
import org.terasology.rendering.assets.animation.MeshAnimationFrame;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
*/
@RegisterAssetFileFormat
public class MD5AnimationLoader extends AbstractAssetFileFormat<MeshAnimationData> {
private static final String INTEGER_PATTERN = "((?:[\\+-]?\\d+)(?:[eE][\\+-]?\\d+)?)";
private static final String FLOAT_PATTERN = "((?:[\\+-]?\\d(?:\\.\\d*)?|\\.\\d+)(?:[eE][\\+-]?(?:\\d(?:\\.\\d*)?|\\.\\d+))?)";
private static final String VECTOR3_PATTERN = "\\(\\s*" + FLOAT_PATTERN + "\\s+" + FLOAT_PATTERN + "\\s+" + FLOAT_PATTERN + "\\s+\\)";
private static final int POSITION_X_FLAG = 0x1;
private static final int POSITION_Y_FLAG = 0x2;
private static final int POSITION_Z_FLAG = 0x4;
private static final int ORIENTATION_X_FLAG = 0x8;
private static final int ORIENTATION_Y_FLAG = 0x10;
private static final int ORIENTATION_Z_FLAG = 0x20;
private Pattern commandLinePattern = Pattern.compile("commandline \"(.*)\".*");
private Pattern jointPattern = Pattern.compile("\"(.*)\"\\s+" + INTEGER_PATTERN + "\\s*" + INTEGER_PATTERN + "\\s*" + INTEGER_PATTERN);
private Pattern doubleVectorPattern = Pattern.compile(VECTOR3_PATTERN + "\\s*" + VECTOR3_PATTERN);
private Pattern frameStartPattern = Pattern.compile("frame " + INTEGER_PATTERN + " \\{");
public MD5AnimationLoader() {
super("md5anim");
}
@Override
public MeshAnimationData load(ResourceUrn urn, List<AssetDataFile> inputs) throws IOException {
try (InputStream stream = inputs.get(0).openStream()) {
MD5 md5 = parse(stream);
return createAnimation(md5);
} catch (NumberFormatException e) {
throw new IOException("Error parsing " + inputs.get(0).getFilename(), e);
}
}
private MeshAnimationData createAnimation(MD5 md5) {
List<String> boneNames = Lists.newArrayListWithCapacity(md5.numJoints);
TIntList boneParents = new TIntArrayList(md5.numJoints);
for (int i = 0; i < md5.numJoints; ++i) {
boneNames.add(md5.joints[i].name);
boneParents.add(md5.joints[i].parent);
}
float timePerFrame = 1.0f / md5.frameRate;
List<MeshAnimationFrame> frames = Lists.newArrayList();
for (int frameIndex = 0; frameIndex < md5.numFrames; ++frameIndex) {
MD5Frame frame = md5.frames[frameIndex];
List<Vector3f> positions = Lists.newArrayListWithExpectedSize(md5.numJoints);
List<Vector3f> rawRotations = Lists.newArrayListWithExpectedSize(md5.numJoints);
for (int i = 0; i < md5.numJoints; ++i) {
positions.add(new Vector3f(md5.baseFramePosition[i]));
rawRotations.add(new Vector3f(md5.baseFrameOrientation[i]));
}
for (int jointIndex = 0; jointIndex < md5.numJoints; ++jointIndex) {
int compIndex = 0;
if ((md5.joints[jointIndex].flags & POSITION_X_FLAG) != 0) {
positions.get(jointIndex).x = frame.components[md5.joints[jointIndex].startIndex + compIndex];
compIndex++;
}
if ((md5.joints[jointIndex].flags & POSITION_Y_FLAG) != 0) {
positions.get(jointIndex).y = frame.components[md5.joints[jointIndex].startIndex + compIndex];
compIndex++;
}
if ((md5.joints[jointIndex].flags & POSITION_Z_FLAG) != 0) {
positions.get(jointIndex).z = frame.components[md5.joints[jointIndex].startIndex + compIndex];
compIndex++;
}
if ((md5.joints[jointIndex].flags & ORIENTATION_X_FLAG) != 0) {
rawRotations.get(jointIndex).x = frame.components[md5.joints[jointIndex].startIndex + compIndex];
compIndex++;
}
if ((md5.joints[jointIndex].flags & ORIENTATION_Y_FLAG) != 0) {
rawRotations.get(jointIndex).y = frame.components[md5.joints[jointIndex].startIndex + compIndex];
compIndex++;
}
if ((md5.joints[jointIndex].flags & ORIENTATION_Z_FLAG) != 0) {
rawRotations.get(jointIndex).z = frame.components[md5.joints[jointIndex].startIndex + compIndex];
}
}
List<Quat4f> rotations = rawRotations.stream().map(rot ->
MD5ParserCommon.completeQuat4f(rot.x, rot.y, rot.z)).collect(Collectors.toCollection(ArrayList::new));
// Rotate just the root bone to correct for coordinate system differences
rotations.set(0, MD5ParserCommon.correctQuat4f(rotations.get(0)));
positions.set(0, MD5ParserCommon.correctOffset(positions.get(0)));
frames.add(new MeshAnimationFrame(positions, rotations));
}
AABB aabb = AABB.createEncompassing(Arrays.asList(md5.bounds));
return new MeshAnimationData(boneNames, boneParents, frames, timePerFrame, aabb);
}
private MD5 parse(InputStream stream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, Charsets.UTF_8));
MD5 md5 = new MD5();
String line = MD5ParserCommon.readToLine(reader, "MD5Version ");
md5.version = Integer.parseInt(line.split(" ", 3)[1]);
line = MD5ParserCommon.readToLine(reader, "commandline ");
Matcher commandlineMatch = commandLinePattern.matcher(line);
if (commandlineMatch.matches()) {
md5.commandline = commandlineMatch.group(1);
}
line = MD5ParserCommon.readToLine(reader, "numFrames ");
md5.numFrames = Integer.parseInt(line.split(" ", 3)[1]);
line = MD5ParserCommon.readToLine(reader, "numJoints ");
md5.numJoints = Integer.parseInt(line.split(" ", 3)[1]);
line = MD5ParserCommon.readToLine(reader, "frameRate ");
md5.frameRate = Integer.parseInt(line.split(" ", 3)[1]);
line = MD5ParserCommon.readToLine(reader, "numAnimatedComponents ");
md5.numAnimatedComponents = Integer.parseInt(line.split(" ", 3)[1]);
MD5ParserCommon.readToLine(reader, "hierarchy {");
readHierarchy(reader, md5);
MD5ParserCommon.readToLine(reader, "bounds {");
readBounds(reader, md5);
MD5ParserCommon.readToLine(reader, "baseframe {");
readBaseFrames(reader, md5);
readFrames(reader, md5);
return md5;
}
private void readFrames(BufferedReader reader, MD5 md5) throws IOException {
md5.frames = new MD5Frame[md5.numFrames];
for (int i = 0; i < md5.numFrames; ++i) {
String frameStart = MD5ParserCommon.readToLine(reader, "frame ");
Matcher frameStartMatcher = frameStartPattern.matcher(frameStart);
if (!frameStartMatcher.find()) {
throw new IOException("Invalid frame line: \"" + frameStart + "\"");
}
int frameIndex = Integer.parseInt(frameStartMatcher.group(1));
MD5Frame frame = new MD5Frame();
frame.components = new float[md5.numAnimatedComponents];
int componentsRead = 0;
while (componentsRead < md5.numAnimatedComponents) {
String line = MD5ParserCommon.readNextLine(reader);
String[] components = line.trim().split("\\s+");
for (String component : components) {
frame.components[componentsRead++] = Float.parseFloat(component);
}
}
md5.frames[frameIndex] = frame;
}
}
private void readBaseFrames(BufferedReader reader, MD5 md5) throws IOException {
md5.baseFramePosition = new Vector3f[md5.numJoints];
md5.baseFrameOrientation = new Vector3f[md5.numJoints];
for (int i = 0; i < md5.numJoints; ++i) {
String line = MD5ParserCommon.readNextLine(reader);
Matcher matcher = doubleVectorPattern.matcher(line);
if (!matcher.find()) {
throw new IOException("Invalid base frame line: \"" + line + "\"");
}
md5.baseFramePosition[i] = MD5ParserCommon.readVector3f(matcher.group(1), matcher.group(2), matcher.group(3));
md5.baseFrameOrientation[i] = MD5ParserCommon.readVector3f(matcher.group(4), matcher.group(5), matcher.group(6));
}
}
private void readBounds(BufferedReader reader, MD5 md5) throws IOException {
md5.bounds = new AABB[md5.numFrames];
for (int i = 0; i < md5.numFrames; ++i) {
String line = MD5ParserCommon.readNextLine(reader);
Matcher matcher = doubleVectorPattern.matcher(line);
if (!matcher.find()) {
throw new IOException("Invalid bounds line: \"" + line + "\"");
}
Vector3f a = MD5ParserCommon.readVector3fAndCorrect(matcher.group(1), matcher.group(2), matcher.group(3));
Vector3f b = MD5ParserCommon.readVector3fAndCorrect(matcher.group(4), matcher.group(5), matcher.group(6));
Vector3f min = new Vector3f();
min.x = Math.min(a.x, b.x);
min.y = Math.min(a.y, b.y);
min.z = Math.min(a.z, b.z);
Vector3f max = new Vector3f();
max.x = Math.max(a.x, b.x);
max.y = Math.max(a.y, b.y);
max.z = Math.max(a.z, b.z);
md5.bounds[i] = AABB.createMinMax(min, max);
}
}
private void readHierarchy(BufferedReader reader, MD5 md5) throws IOException {
md5.joints = new MD5Joint[md5.numJoints];
for (int i = 0; i < md5.numJoints; ++i) {
String line = MD5ParserCommon.readNextLine(reader);
Matcher matcher = jointPattern.matcher(line);
if (!matcher.find()) {
throw new IOException("Invalid joint line: \"" + line + "\"");
}
MD5Joint joint = new MD5Joint();
joint.name = matcher.group(1);
joint.parent = Integer.parseInt(matcher.group(2));
joint.flags = Integer.parseInt(matcher.group(3));
joint.startIndex = Integer.parseInt(matcher.group(4));
md5.joints[i] = joint;
}
}
private static class MD5 {
public int version;
public String commandline;
public int numFrames;
public int numJoints;
public int frameRate;
public int numAnimatedComponents;
public MD5Joint[] joints;
public AABB[] bounds;
public Vector3f[] baseFramePosition;
public Vector3f[] baseFrameOrientation;
public MD5Frame[] frames;
}
public static class MD5Joint {
public String name;
public int parent;
public int flags;
public int startIndex;
}
public static class MD5Frame {
public float[] components;
}
}