mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Add basic BVH file streamer
This commit is contained in:
@@ -7,5 +7,5 @@ root = true
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
#trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
184
src/main/java/dev/slimevr/poserecorder/BVHFileStream.java
Normal file
184
src/main/java/dev/slimevr/poserecorder/BVHFileStream.java
Normal file
@@ -0,0 +1,184 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import io.eiren.vr.processor.HumanSkeleton;
|
||||
import io.eiren.vr.processor.TransformNode;
|
||||
|
||||
public class BVHFileStream extends PoseFileStream {
|
||||
|
||||
private static final int LONG_MAX_VALUE_DIGITS = Long.toString(Long.MAX_VALUE).length();
|
||||
private static final float POS_SCALE = 10f;
|
||||
|
||||
private long frameCount = 0;
|
||||
private final BufferedWriter writer;
|
||||
|
||||
private long frameCountOffset;
|
||||
|
||||
private float[] angleBuf = new float[3];
|
||||
private Quaternion rotBuf = new Quaternion();
|
||||
|
||||
public BVHFileStream(OutputStream outputStream) {
|
||||
super(outputStream);
|
||||
writer = new BufferedWriter(new OutputStreamWriter(outputStream), 128);
|
||||
}
|
||||
|
||||
public BVHFileStream(File file) throws FileNotFoundException {
|
||||
super(file);
|
||||
writer = new BufferedWriter(new OutputStreamWriter(outputStream), 128);
|
||||
}
|
||||
|
||||
public BVHFileStream(String file) throws FileNotFoundException {
|
||||
super(file);
|
||||
writer = new BufferedWriter(new OutputStreamWriter(outputStream), 128);
|
||||
}
|
||||
|
||||
private String getBufferedFrameCount(long frameCount) {
|
||||
String frameString = Long.toString(frameCount);
|
||||
int bufferCount = LONG_MAX_VALUE_DIGITS - frameString.length();
|
||||
|
||||
return bufferCount > 0 ? frameString + StringUtils.repeat(' ', bufferCount) : frameString;
|
||||
}
|
||||
|
||||
private void writeTransformNodeHierarchy(TransformNode node) throws IOException {
|
||||
writeTransformNodeHierarchy(node, 0);
|
||||
}
|
||||
|
||||
private void writeTransformNodeHierarchy(TransformNode node, int level) throws IOException {
|
||||
String indentLevel = StringUtils.repeat("\t", level);
|
||||
String nextIndentLevel = indentLevel + "\t";
|
||||
|
||||
// Handle ends
|
||||
if (node.children.isEmpty()) {
|
||||
writer.write(indentLevel + "End Site\n");
|
||||
} else {
|
||||
writer.write((level > 0 ? indentLevel + "JOINT " : "ROOT ") + node.getName() + "\n");
|
||||
}
|
||||
writer.write(indentLevel + "{\n");
|
||||
|
||||
if (level > 0) {
|
||||
Vector3f offset = node.localTransform.getTranslation();
|
||||
writer.write(nextIndentLevel + "OFFSET " + Float.toString(offset.getX() * POS_SCALE) + " " + Float.toString(offset.getY() * POS_SCALE) + " " + Float.toString(offset.getZ() * POS_SCALE) + "\n");
|
||||
} else {
|
||||
writer.write(nextIndentLevel + "OFFSET 0.0 0.0 0.0\n");
|
||||
}
|
||||
|
||||
// Handle ends
|
||||
if (!node.children.isEmpty()) {
|
||||
// Only give position for root
|
||||
if (level > 0) {
|
||||
writer.write(nextIndentLevel + "CHANNELS 3 Zrotation Xrotation Yrotation\n");
|
||||
} else {
|
||||
writer.write(nextIndentLevel + "CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n");
|
||||
}
|
||||
|
||||
for (TransformNode childNode : node.children) {
|
||||
writeTransformNodeHierarchy(childNode, level + 1);
|
||||
}
|
||||
}
|
||||
|
||||
writer.write(indentLevel + "}\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(HumanSkeleton skeleton, PoseStreamer streamer) throws IOException {
|
||||
if (skeleton == null) {
|
||||
throw new NullPointerException("skeleton must not be null");
|
||||
}
|
||||
if (streamer == null) {
|
||||
throw new NullPointerException("streamer must not be null");
|
||||
}
|
||||
|
||||
writer.write("HIERARCHY\n");
|
||||
writeTransformNodeHierarchy(skeleton.getRootNode());
|
||||
|
||||
writer.write("MOTION\n");
|
||||
writer.write("Frames: ");
|
||||
|
||||
// Get frame offset for finishing writing the file
|
||||
if (parentStream instanceof FileOutputStream) {
|
||||
FileOutputStream fileOutputStream = (FileOutputStream)parentStream;
|
||||
// Flush buffer to get proper offset
|
||||
writer.flush();
|
||||
frameCountOffset = fileOutputStream.getChannel().position();
|
||||
}
|
||||
|
||||
writer.write(getBufferedFrameCount(frameCount) + "\n");
|
||||
|
||||
// Frame time in seconds
|
||||
writer.write("Frame Time: " + (streamer.frameRecordingInterval / 1000d) + "\n");
|
||||
}
|
||||
|
||||
private void writeTransformHierarchyRotation(TransformNode node, Quaternion inverseRootRot) throws IOException {
|
||||
rotBuf = node.localTransform.getRotation(rotBuf);
|
||||
|
||||
// Adjust to local rotation
|
||||
if (inverseRootRot != null) {
|
||||
rotBuf = inverseRootRot.mult(rotBuf, rotBuf);
|
||||
}
|
||||
|
||||
angleBuf = rotBuf.toAngles(angleBuf);
|
||||
writer.write(Float.toString((float)Math.toDegrees(angleBuf[2])) + " " + Float.toString((float)Math.toDegrees(angleBuf[0])) + " " + Float.toString((float)Math.toDegrees(angleBuf[1])));
|
||||
|
||||
// Get inverse rotation for child local rotations
|
||||
Quaternion inverseRot = node.localTransform.getRotation().inverse();
|
||||
for (TransformNode childNode : node.children) {
|
||||
if (childNode.children.isEmpty()) {
|
||||
// If it's an end node, skip
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add spacing
|
||||
writer.write(" ");
|
||||
writeTransformHierarchyRotation(childNode, inverseRot);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFrame(HumanSkeleton skeleton) throws IOException {
|
||||
if (skeleton == null) {
|
||||
throw new NullPointerException("skeleton must not be null");
|
||||
}
|
||||
|
||||
TransformNode root = skeleton.getRootNode();
|
||||
Vector3f rootPos = root.localTransform.getTranslation();
|
||||
|
||||
// Write root position
|
||||
writer.write(Float.toString(rootPos.getX() * POS_SCALE) + " " + Float.toString(rootPos.getY() * POS_SCALE) + " " + Float.toString(rootPos.getZ() * POS_SCALE) + " ");
|
||||
writeTransformHierarchyRotation(root, null);
|
||||
|
||||
writer.newLine();
|
||||
|
||||
frameCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter(HumanSkeleton skeleton) throws IOException {
|
||||
// Write the final frame count for files
|
||||
if (parentStream instanceof FileOutputStream) {
|
||||
FileOutputStream fileOutputStream = (FileOutputStream)parentStream;
|
||||
// Flush before anything else
|
||||
writer.flush();
|
||||
// Seek to the count offset
|
||||
fileOutputStream.getChannel().position(frameCountOffset);
|
||||
// Overwrite the count with a new value
|
||||
writer.write(Long.toString(frameCount));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package dev.slimevr.poserecorder;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -11,11 +10,13 @@ import java.io.OutputStream;
|
||||
import io.eiren.vr.processor.HumanSkeleton;
|
||||
|
||||
public abstract class PoseFileStream implements AutoCloseable {
|
||||
|
||||
protected DataOutputStream dataStream;
|
||||
|
||||
protected final OutputStream parentStream;
|
||||
protected final BufferedOutputStream outputStream;
|
||||
|
||||
protected PoseFileStream(OutputStream outputStream) {
|
||||
this.dataStream = new DataOutputStream(new BufferedOutputStream(outputStream));
|
||||
this.parentStream = outputStream;
|
||||
this.outputStream = new BufferedOutputStream(outputStream);
|
||||
}
|
||||
|
||||
protected PoseFileStream(File file) throws FileNotFoundException {
|
||||
@@ -26,14 +27,16 @@ public abstract class PoseFileStream implements AutoCloseable {
|
||||
this(new FileOutputStream(file));
|
||||
}
|
||||
|
||||
abstract boolean writeHeader(HumanSkeleton skeleton) throws IOException;
|
||||
public void writeHeader(HumanSkeleton skeleton, PoseStreamer streamer) throws IOException {
|
||||
}
|
||||
|
||||
abstract boolean writeFrame(HumanSkeleton skeleton) throws IOException;
|
||||
abstract void writeFrame(HumanSkeleton skeleton) throws IOException;
|
||||
|
||||
abstract boolean writeFooter(HumanSkeleton skeleton) throws IOException;
|
||||
public void writeFooter(HumanSkeleton skeleton) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
dataStream.close();
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,32 +63,27 @@ public class PoseStreamer {
|
||||
}
|
||||
|
||||
public void setOutput(PoseFileStream poseFileStream) throws IOException {
|
||||
poseFileStream.writeHeader(skeleton);
|
||||
poseFileStream.writeHeader(skeleton, this);
|
||||
this.poseFileStream = poseFileStream;
|
||||
nextFrameTimeMs = -1L; // Reset the frame timing
|
||||
}
|
||||
|
||||
public void setOutput(PoseFileStream poseFileStream, long intervalMs) throws IOException {
|
||||
setFrameInterval(intervalMs);
|
||||
setOutput(poseFileStream);
|
||||
}
|
||||
|
||||
public PoseFileStream getOutput() {
|
||||
return poseFileStream;
|
||||
}
|
||||
|
||||
public void closeOutput() {
|
||||
public void closeOutput() throws IOException {
|
||||
PoseFileStream poseFileStream = this.poseFileStream;
|
||||
|
||||
if (poseFileStream != null) {
|
||||
try {
|
||||
poseFileStream.writeFooter(skeleton);
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
LogManager.log.severe("[PoseStreamer] Exception while writing file footer", e);
|
||||
}
|
||||
|
||||
try {
|
||||
poseFileStream.close();
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
LogManager.log.severe("[PoseStreamer] Exception while closing file stream", e);
|
||||
}
|
||||
if (poseFileStream != null) {
|
||||
poseFileStream.writeFooter(skeleton);
|
||||
poseFileStream.close();
|
||||
this.poseFileStream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user