More progress, working on getting viewer

This commit is contained in:
Retera 2019-11-24 16:02:20 -06:00
parent f0da595d20
commit 7175a28753
28 changed files with 1680 additions and 0 deletions

View File

@ -0,0 +1,46 @@
package com.etheller.warsmash.datasources;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
public interface DataSource {
/**
* Efficiently return a stream instance that will read the data source file's
* contents directly from the data source. For example, this will read a file
* within an MPQ or CASC storage without extracting it.
*
* @param filepath
* @return
* @throws IOException
*/
InputStream getResourceAsStream(String filepath) throws IOException;
/**
* Inefficiently copy a file from the data source onto the Hard Drive of the
* computer, and then return a java File instance pointed at the file.
*
* @param filepath
* @return
* @throws IOException
*/
File getFile(String filepath) throws IOException;
/**
* Returns true if the data source contains a valid entry for a particular file.
* Some data sources (MPQs) may contain files for which this returns true, even
* though they cannot list the file in their Listfile.
*
* @param filepath
* @return
*/
boolean has(String filepath);
/**
* @return a list of data source contents, or null if no list is provided
*/
Collection<String> getListfile();
void close() throws IOException;
}

View File

@ -0,0 +1,9 @@
package com.etheller.warsmash.datasources;
import java.io.Serializable;
public interface DataSourceDescriptor extends Serializable {
DataSource createDataSource();
String getDisplayName();
}

View File

@ -0,0 +1,68 @@
package com.etheller.warsmash.datasources;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
public class FolderDataSource implements DataSource {
private final Path folderPath;
private final Set<String> listfile;
public FolderDataSource(final Path folderPath) {
this.folderPath = folderPath;
this.listfile = new HashSet<>();
try {
Files.walk(folderPath).filter(Files::isRegularFile).forEach(new Consumer<Path>() {
@Override
public void accept(final Path t) {
FolderDataSource.this.listfile.add(folderPath.relativize(t).toString());
}
});
}
catch (final IOException e) {
throw new RuntimeException(e);
}
}
@Override
public InputStream getResourceAsStream(final String filepath) throws IOException {
if (!has(filepath)) {
return null;
}
return Files.newInputStream(this.folderPath.resolve(filepath), StandardOpenOption.READ);
}
@Override
public File getFile(final String filepath) throws IOException {
if (!has(filepath)) {
return null;
}
return new File(this.folderPath.toString() + File.separatorChar + filepath);
}
@Override
public boolean has(final String filepath) {
if ("".equals(filepath)) {
return false; // special case for folder data source, dont do this
}
return Files.exists(this.folderPath.resolve(filepath));
}
@Override
public Collection<String> getListfile() {
return this.listfile;
}
@Override
public void close() {
}
}

View File

@ -0,0 +1,57 @@
package com.etheller.warsmash.datasources;
import java.nio.file.Paths;
public class FolderDataSourceDescriptor implements DataSourceDescriptor {
/**
* Generated serial id
*/
private static final long serialVersionUID = -476724730967709309L;
private final String folderPath;
public FolderDataSourceDescriptor(final String folderPath) {
this.folderPath = folderPath;
}
@Override
public DataSource createDataSource() {
return new FolderDataSource(Paths.get(this.folderPath));
}
@Override
public String getDisplayName() {
return "Folder: " + this.folderPath;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result) + ((this.folderPath == null) ? 0 : this.folderPath.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final FolderDataSourceDescriptor other = (FolderDataSourceDescriptor) obj;
if (this.folderPath == null) {
if (other.folderPath != null) {
return false;
}
}
else if (!this.folderPath.equals(other.folderPath)) {
return false;
}
return true;
}
}

View File

@ -1,5 +1,7 @@
package com.etheller.warsmash.util;
import java.util.List;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Rectangle;
@ -287,4 +289,46 @@ public enum RenderMathUtils {
return out;
}
public static int testCell(final List<Vector4> planes, final int left, final int right, final int bottom,
final int top, int first) {
if (first == -1) {
first = 0;
}
for (int i = 0; i < 6; i++) {
final int index = (first + i) % 6;
final Vector4 plane = planes.get(index);
if ((distance2Plane2(plane, left, bottom) < 0) && (distance2Plane2(plane, left, top) < 0)
&& (distance2Plane2(plane, right, top) < 0) && (distance2Plane2(plane, right, bottom) < 0)) {
return index;
}
}
return -1;
}
public static int testCell(final Vector4[] planes, final int left, final int right, final int bottom, final int top,
int first) {
if (first == -1) {
first = 0;
}
for (int i = 0; i < 6; i++) {
final int index = (first + i) % 6;
final Vector4 plane = planes[index];
if ((distance2Plane2(plane, left, bottom) < 0) && (distance2Plane2(plane, left, top) < 0)
&& (distance2Plane2(plane, right, top) < 0) && (distance2Plane2(plane, right, bottom) < 0)) {
return index;
}
}
return -1;
}
public static float distance2Plane2(final Vector4 plane, final int px, final int py) {
return (plane.x * px) + (plane.y * py) + plane.w;
}
}

View File

@ -0,0 +1,47 @@
package com.etheller.warsmash.viewer5;
public class AudioContext {
private boolean running = false;
public Listener listener = new Listener();
public void suspend() {
this.running = false;
}
public boolean isRunning() {
return this.running;
}
public void resume() {
this.running = true;
}
public static class Listener {
private float x;
private float y;
private float z;
private float forwardX;
private float forwardY;
private float forwardZ;
private float upX;
private float upY;
private float upZ;
public void setPosition(final float x, final float y, final float z) {
this.x = x;
this.y = y;
this.z = z;
}
public void setOrientation(final float forwardX, final float forwardY, final float forwardZ, final float upX,
final float upY, final float upZ) {
this.forwardX = forwardX;
this.forwardY = forwardY;
this.forwardZ = forwardZ;
this.upX = upX;
this.upY = upY;
this.upZ = upZ;
}
}
}

View File

@ -0,0 +1,5 @@
package com.etheller.warsmash.viewer5;
public class Bounds {
public int x, y, r;
}

View File

@ -0,0 +1,326 @@
package com.etheller.warsmash.viewer5;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.etheller.warsmash.util.RenderMathUtils;
import com.etheller.warsmash.util.Vector4;
public class Camera {
private static final Vector3 vectorHeap = new Vector3();
private static final Vector3 vectorHeap2 = new Vector3();
private static final Vector3 vectorHeap3 = new Vector3();
private static final Quaternion quatHeap = new Quaternion();
private static final Matrix4 matHeap = new Matrix4();
public final Rectangle rect;
private boolean isPerspective;
private float fov;
private float aspect;
private boolean isOrtho;
private float leftClipPlane;
private float rightClipPlane;
private float bottomClipPlane;
private float topClipPlane;
private float nearClipPlane;
private float farClipPlane;
public final Vector3 location;
public final Quaternion rotation;
public Quaternion inverseRotation;
private final Matrix4 worldMatrix;
private final Matrix4 projectionMatrix;
private final Matrix4 worldProjectionMatrix;
private final Matrix4 inverseWorldMatrix;
private final Matrix4 inverseRotationMatrix;
private final Matrix4 inverseWorldProjectionMatrix;
public final Vector3 directionX;
public final Vector3 directionY;
public final Vector3 directionZ;
private final Vector3[] vectors;
private final Vector3[] billboardedVectors;
public final Vector4[] planes;
private boolean dirty;
public Camera() {
// rencered viewport
this.rect = new Rectangle();
// perspective values
this.isPerspective = true;
this.fov = 0;
this.aspect = 0;
// Orthogonal values
this.isOrtho = false;
this.leftClipPlane = 0f;
this.rightClipPlane = 0f;
this.bottomClipPlane = 0f;
this.topClipPlane = 0f;
// Shared values
this.nearClipPlane = 0f;
this.farClipPlane = 0f;
// World values
this.location = new Vector3();
this.rotation = new Quaternion();
// Derived values.
this.inverseRotation = new Quaternion();
this.worldMatrix = new Matrix4();
this.projectionMatrix = new Matrix4();
this.worldProjectionMatrix = new Matrix4();
this.inverseWorldMatrix = new Matrix4();
this.inverseRotationMatrix = new Matrix4();
this.inverseWorldProjectionMatrix = new Matrix4();
this.directionX = new Vector3();
this.directionY = new Vector3();
this.directionZ = new Vector3();
// First four vectors are the corners of a 2x2 rectangle, the last three vectors
// are the unit axes
this.vectors = new Vector3[] { new Vector3(-1, -1, 0), new Vector3(-1, 1, 0), new Vector3(1, 1, 0),
new Vector3(1, -1, 0), new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1) };
// First four vectors are the corners of a 2x2 rectangle billboarded to the
// camera, the last three vectors are the unit axes billboarded
this.billboardedVectors = new Vector3[] { new Vector3(), new Vector3(), new Vector3(), new Vector3(),
new Vector3(), new Vector3(), new Vector3() };
// Left, right, top, bottom, near, far
this.planes = new Vector4[] { new Vector4(), new Vector4(), new Vector4(), new Vector4(), new Vector4(),
new Vector4() };
this.dirty = true;
}
public void perspective(final float fov, final float aspect, final float near, final float far) {
this.isPerspective = true;
this.isOrtho = false;
this.fov = fov;
this.aspect = aspect;
this.nearClipPlane = near;
this.farClipPlane = far;
this.dirty = true;
}
public void ortho(final float left, final float right, final float bottom, final float top, final float near,
final float far) {
this.isPerspective = false;
this.isOrtho = true;
this.leftClipPlane = left;
this.rightClipPlane = right;
this.bottomClipPlane = bottom;
this.topClipPlane = top;
this.nearClipPlane = near;
this.farClipPlane = far;
}
public void viewport(final Rectangle viewport) {
this.rect.set(viewport);
this.aspect = viewport.width / viewport.height;
this.dirty = true;
}
public void setLocation(final Vector3 location) {
this.location.set(location);
this.dirty = true;
}
public void move(final Vector3 offset) {
this.location.add(offset);
this.dirty = true;
}
public void setRotation(final Quaternion rotation) {
this.rotation.set(rotation);
this.dirty = true;
}
public void rotate(final Quaternion rotation) {
this.rotation.mul(rotation);
this.dirty = true;
}
public void setRotationAngles(final float horizontalAngle, final float verticalAngle) {
this.rotation.idt();
// this.rotateAngles(horizontalAngle, verticalAngle);
throw new UnsupportedOperationException(
"Ghostwolf called a function that does not exist, so I did not know what to do here");
}
public void rotateAround(final Quaternion rotation, final Vector3 point) {
this.rotate(rotation);
quatHeap.conjugate(); // TODO ?????????
vectorHeap.set(this.location);
vectorHeap.sub(point);
rotation.transform(vectorHeap);
vectorHeap.add(point);
this.location.set(vectorHeap);
}
public void setRotationAround(final Quaternion rotation, final Vector3 point) {
this.setRotation(rotation);
;
final float length = vectorHeap.set(this.location).sub(point).len();
quatHeap.conjugate(); // TODO ?????????
vectorHeap.set(RenderMathUtils.VEC3_UNIT_Z);
quatHeap.transform(vectorHeap);
vectorHeap.scl(length);
this.location.set(vectorHeap.add(point));
}
public void setRotationAroundAngles(final float horizontalAngle, final float verticalAngle, final Vector3 point) {
quatHeap.idt();
RenderMathUtils.rotateX(quatHeap, quatHeap, verticalAngle);
RenderMathUtils.rotateY(quatHeap, quatHeap, horizontalAngle);
this.setRotationAround(quatHeap, point);
}
public void face(final Vector3 point, final Vector3 worldUp) {
matHeap.setToLookAt(this.location, point, worldUp);
matHeap.getRotation(this.rotation);
this.dirty = true;
}
public void moveToAndFace(final Vector3 location, final Vector3 target, final Vector3 worldUp) {
this.location.set(location);
this.face(target, worldUp);
}
public void reset() {
this.location.set(0, 0, 0);
this.rotation.idt();
this.dirty = true;
}
public void update() {
if (this.dirty) {
this.dirty = true;
final Vector3 location = this.location;
final Quaternion rotation = this.rotation;
final Quaternion inverseRotation = this.inverseRotation;
final Matrix4 worldMatrix = this.worldMatrix;
final Matrix4 projectionMatrix = this.projectionMatrix;
final Matrix4 worldProjectionMatrix = this.worldProjectionMatrix;
final Vector3[] vectors = this.vectors;
final Vector3[] billboardedVectors = this.billboardedVectors;
if (this.isPerspective) {
RenderMathUtils.perspective(projectionMatrix, this.fov, this.aspect, this.nearClipPlane,
this.farClipPlane);
}
else {
RenderMathUtils.ortho(projectionMatrix, this.leftClipPlane, this.rightClipPlane, this.bottomClipPlane,
this.topClipPlane, this.nearClipPlane, this.farClipPlane);
}
rotation.toMatrix(projectionMatrix.val);
worldMatrix.translate(vectorHeap.set(location).scl(-1));
inverseRotation.set(rotation).conjugate();
// World projection matrix
// World space -> NDC space
worldProjectionMatrix.set(projectionMatrix).mul(worldMatrix);
// Recalculate the camera's frustum planes
RenderMathUtils.unpackPlanes(this.planes, worldProjectionMatrix);
// Inverse world matrix
// Camera space -> world space
this.inverseWorldMatrix.set(worldMatrix).inv();
this.directionX.set(RenderMathUtils.VEC3_UNIT_X);
inverseRotation.transform(this.directionX);
this.directionY.set(RenderMathUtils.VEC3_UNIT_Y);
inverseRotation.transform(this.directionY);
this.directionZ.set(RenderMathUtils.VEC3_UNIT_Z);
inverseRotation.transform(this.directionZ);
// Inverse world projection matrix
// NDC space -> World space
this.inverseWorldProjectionMatrix.set(worldProjectionMatrix);
this.inverseWorldProjectionMatrix.inv();
for (int i = 0; i < 7; i++) {
billboardedVectors[i].set(vectors[i]);
inverseRotation.transform(billboardedVectors[i]);
}
}
}
public boolean testSphere(final Vector3 center, final float radius) {
for (final Vector4 plane : this.planes) {
if (RenderMathUtils.distanceToPlane(plane, center) <= -radius) {
return false;
}
}
return true;
}
public Vector3 cameraToWorld(final Vector3 out, final Vector3 v) {
return out.set(v).prj(this.inverseWorldMatrix);
}
public Vector3 worldToCamera(final Vector3 out, final Vector3 v) {
return out.set(v).prj(this.worldMatrix);
}
public Vector2 worldToScreen(final Vector2 out, final Vector3 v) {
final Rectangle viewport = this.rect;
vectorHeap.set(v);
vectorHeap.prj(this.inverseWorldMatrix);
out.x = Math.round(((vectorHeap.x + 1) / 2) * viewport.width);
out.y = Math.round(((vectorHeap.y + 1) / 2) * viewport.height);
return out;
}
public float[] screenToWorldRay(final float[] out, final Vector2 v) {
final Vector3 a = vectorHeap;
final Vector3 b = vectorHeap2;
final Vector3 c = vectorHeap3;
final float x = v.x;
final float y = v.y;
final Rectangle viewport = this.rect;
// Intersection on the near-plane
RenderMathUtils.unproject(a, c.set(x, y, 0), this.inverseWorldProjectionMatrix, viewport);
// Intersection on the far-plane
RenderMathUtils.unproject(b, c.set(x, y, 1), this.inverseWorldProjectionMatrix, viewport);
out[0] = a.x;
out[1] = a.y;
out[2] = a.z;
out[3] = b.x;
out[4] = b.y;
out[5] = b.z;
return out;
}
}

View File

@ -0,0 +1,7 @@
package com.etheller.warsmash.viewer5;
public interface CanvasProvider {
float getWidth();
float getHeight();
}

View File

@ -0,0 +1,11 @@
package com.etheller.warsmash.viewer5;
public abstract class EmittedObject {
abstract void update(float dt);
public float health;
public Emitter emitter;
public int index;
protected abstract void bind(int flags);
}

View File

@ -0,0 +1,40 @@
package com.etheller.warsmash.viewer5;
import java.util.ArrayList;
import java.util.List;
public class EmittedObjectUpdater {
final List<EmittedObject> objects;
private int alive;
public EmittedObjectUpdater() {
this.objects = new ArrayList<>();
this.alive = 0;
}
public void add(final EmittedObject object) {
this.objects.add(object);
this.alive++;
}
public void update(final float dt) {
for (int i = 0; i < this.alive; i++) {
final EmittedObject object = this.objects.get(i);
object.update(dt);
if (object.health <= 0) {
this.alive -= 1;
object.emitter.kill(object);
// Swap between this object and the last living object.
// Decrement the iterator so the swapped object is updated this frame.
if (i != this.alive) {
this.objects.set(i, this.objects.remove(this.alive));
i -= 1;
}
}
}
}
}

View File

@ -0,0 +1,75 @@
package com.etheller.warsmash.viewer5;
import java.util.ArrayList;
import java.util.List;
import com.etheller.warsmash.viewer5.handlers.EmitterObject;
public abstract class Emitter {
private final ModelInstance instance;
private final EmitterObject emitterObject;
private final List<EmittedObject> objects;
private int alive;
private int currentEmission;
public Emitter(final ModelInstance instance, final EmitterObject emitterObject) {
this.instance = instance;
this.emitterObject = emitterObject;
this.objects = new ArrayList<>();
this.alive = 0;
this.currentEmission = 0;
}
public final EmittedObject emitObject(final int flags) {
if (this.alive == this.objects.size()) {
this.objects.add(this.createObject());
}
final EmittedObject object = this.objects.get(this.alive);
object.index = this.alive;
object.bind(flags);
this.alive += 1;
this.currentEmission -= 1;
this.instance.scene.emitterObjectUpdater.add(object);
return object;
}
public final void update(final float dt) {
this.updateEmission(dt);
final int currentEmission = this.currentEmission;
if (currentEmission >= 1) {
for (int i = 0; i < currentEmission; i += 1) {
this.emit();
}
}
}
public final void kill(final EmittedObject object) {
this.alive -= 1;
final EmittedObject otherObject = this.objects.get(this.alive);
this.objects.set(object.index, otherObject);
this.objects.set(this.alive, object);
otherObject.index = object.index;
object.index = -1;
}
public final void clear() {
for (int i = 0; i < this.alive; i++) {
this.objects.get(i).health = 0;
}
this.currentEmission = 0;
}
protected abstract void updateEmission(float dt);
protected abstract void emit();
protected abstract EmittedObject createObject();
}

View File

@ -0,0 +1,119 @@
package com.etheller.warsmash.viewer5;
import com.badlogic.gdx.math.Vector3;
public class Grid {
private final int x;
private final int y;
private final int width;
private final int depth;
private final int cellWidth;
private final int cellDepth;
private final int columns;
private final int rows;
final GridCell[] cells;
public Grid(final int x, int y, final int width, final int depth, final int cellWidth, final int cellDepth) {
final int columns = width / cellWidth;
final int rows = depth / cellDepth;
this.x = x;
this.y = y;
this.width = width;
this.depth = depth;
this.cellWidth = cellWidth;
this.cellDepth = cellDepth;
this.columns = columns;
this.rows = rows;
this.cells = new GridCell[rows * columns];
for (int row = 0; row < rows; row++) {
for (int column = 0; column < columns; column++) {
final int left = x + (columns * cellWidth);
final int right = left + cellWidth;
final int bottom = y = row * cellDepth;
final int top = bottom + cellDepth;
this.cells[(row * columns) + column] = new GridCell(left, right, bottom, top);
}
}
}
public void add(final ModelInstance instance) {
final int left = instance.left;
final int right = instance.right + 1;
final int bottom = instance.bottom;
final int top = instance.top + 1;
if (left != -1) {
for (int y = bottom; y < top; y++) {
for (int x = left; x < right; x++) {
this.cells[(y * this.columns) + x].add(instance);
}
}
}
}
public void remove(final ModelInstance instance) {
final int left = instance.left;
final int right = instance.right + 1;
final int bottom = instance.bottom;
final int top = instance.top + 1;
if (left != -1) {
instance.left = -1;
for (int y = bottom; y < top; y++) {
for (int x = left; x < right; x++) {
this.cells[(y * this.columns) + x].remove(instance);
}
}
}
}
public void moved(final ModelInstance instance) {
final Bounds bounds = instance.model.bounds;
final float x = (instance.worldLocation.x + bounds.x) - this.x;
final float y = (instance.worldLocation.y + bounds.y) - this.y;
final float r = bounds.r;
final Vector3 s = instance.worldScale;
int left = (int) (Math.floor((x - (r * s.x)) / this.cellWidth));
int right = (int) (Math.floor((x + (r * s.x)) / this.cellWidth));
int bottom = (int) (Math.floor((y - (r * s.y)) / this.cellDepth));
int top = (int) (Math.floor((y + (r * s.y)) / this.cellDepth));
if ((right < 0) || (left > (this.columns - 1)) || (top < 0) || (bottom > (this.rows - 1))) {
// The instance is outside of the grid, so remove it.
this.remove(instance);
}
else {
// Clamp the values so they are in the grid.
left = Math.max(left, 0);
right = Math.min(right, this.columns - 1);
bottom = Math.max(bottom, 0);
top = Math.min(top, this.rows - 1);
// If the values actually changed, update the cells.
if ((left != instance.left) || (right != instance.right) || (bottom != instance.bottom)
|| (top != instance.top)) {
/// TODO: This can be optimized by checking if there are shared cells.
/// That can be done in precisely the same way as done a few lines above, i.e.
/// simple rectangle intersection.
this.remove(instance);
instance.left = left;
instance.right = right;
instance.bottom = bottom;
instance.top = top;
this.add(instance);
}
}
}
public void clear() {
for (final GridCell cell : this.cells) {
cell.clear();
}
}
}

View File

@ -0,0 +1,44 @@
package com.etheller.warsmash.viewer5;
import java.util.ArrayList;
import java.util.List;
import com.etheller.warsmash.util.RenderMathUtils;
public class GridCell {
public final int left;
public final int right;
public final int bottom;
public final int top;
public int plane;
final List<ModelInstance> instances;
public final boolean visible;
public GridCell(final int left, final int right, final int bottom, final int top) {
this.left = left;
this.right = right;
this.bottom = bottom;
this.top = top;
this.plane = -1;
this.instances = new ArrayList<ModelInstance>();
this.visible = false;
}
public void add(final ModelInstance instance) {
this.instances.add(instance);
}
public void remove(final ModelInstance instance) {
this.instances.remove(instance);
}
public void clear() {
this.instances.clear();
}
public boolean isVisible(final Camera camera) {
this.plane = RenderMathUtils.testCell(camera.planes, this.left, this.right, this.bottom, this.top, this.plane);
return this.plane == -1;
}
}

View File

@ -0,0 +1,9 @@
package com.etheller.warsmash.viewer5;
import com.etheller.warsmash.viewer5.handlers.Handler;
public class Model {
public Bounds bounds;
public boolean ok;
public Handler handler;
}

View File

@ -0,0 +1,47 @@
package com.etheller.warsmash.viewer5;
import com.badlogic.gdx.math.Vector3;
public class ModelInstance {
public Model model;
public TextureMapper textureMapper;
public int left = -1;
public int right = -1;
public int bottom = -1;
public int top = -1;
public int plane = -1;
public int depth = 0;
public Vector3 worldLocation;
public Vector3 worldScale;
public Scene scene;
public boolean rendered;
public int cullFrame;
public int updateFrame;
public boolean isVisible(final Camera camera) {
// TODO Auto-generated method stub
return false;
}
public void update(final float dt, final Scene scene) {
// TODO Auto-generated method stub
}
public boolean isBatched() {
// TODO Auto-generated method stub
return false;
}
public void renderOpaque() {
// TODO Auto-generated method stub
}
public void renderTranslucent() {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,169 @@
package com.etheller.warsmash.viewer5;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.etheller.warsmash.datasources.DataSource;
import com.etheller.warsmash.viewer5.gl.WebGL;
public class ModelViewer {
private final DataSource dataSource;
public final CanvasProvider canvas;
public List<Resource> resources;
public Map<String, Resource> fetchCache;
public int frameTime;
public GL20 gl;
public WebGL webGL;
public List<Scene> scenes;
private int visibleCells;
private int visibleInstances;
private int updatedParticles;
public int frame;
private final int rectBuffer;
private final boolean enableAudio;
private final Map<Model, List<TextureMapper>> textureMappers;
public ModelViewer(final DataSource dataSource, final CanvasProvider canvas) {
this.dataSource = dataSource;
this.canvas = canvas;
this.resources = new ArrayList<>();
this.fetchCache = new HashMap<>();
this.frameTime = 1000 / 60;
this.gl = Gdx.gl;
this.webGL = new WebGL(this.gl);
this.scenes = new ArrayList<>();
this.visibleCells = 0;
this.visibleInstances = 0;
this.updatedParticles = 0;
this.frame = 0;
this.rectBuffer = this.gl.glGenBuffer();
this.gl.glBindBuffer(GL20.GL_ARRAY_BUFFER, this.rectBuffer);
final ByteBuffer temp = ByteBuffer.allocate(6);
temp.put((byte) 0);
temp.put((byte) 1);
temp.put((byte) 2);
temp.put((byte) 0);
temp.put((byte) 2);
temp.put((byte) 3);
temp.clear();
this.gl.glBufferData(GL20.GL_ARRAY_BUFFER, temp.capacity(), temp, GL20.GL_STATIC_DRAW);
this.enableAudio = false;
this.textureMappers = new HashMap<Model, List<TextureMapper>>();
}
public Scene addScene() {
final Scene scene = new Scene(this);
this.scenes.add(scene);
return scene;
}
public boolean removeScene(final Scene scene) {
return this.scenes.remove(scene);
}
public void clear() {
this.scenes.clear();
}
public boolean has(final String key) {
return this.fetchCache.containsKey(key);
}
public Resource get(final String key) {
return this.fetchCache.get(key);
}
public void updateAndRender() {
update();
}
// public Resource loadGeneric(String path, String dataType, )
public void update() {
final float dt = this.frameTime * 0.001f;
this.frame += 1;
this.visibleCells = 0;
this.visibleInstances = 0;
this.updatedParticles = 0;
for (final Scene scene : this.scenes) {
scene.update(dt);
this.visibleCells += scene.visibleCells;
this.visibleInstances += scene.visibleInstances;
this.updatedParticles += scene.updatedParticles;
}
}
public void startFrame() {
this.gl.glDepthMask(true);
this.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
}
public void render() {
this.renderOpaque();
this.renderTranslucent();
}
private void renderOpaque() {
for (final Scene scene : this.scenes) {
scene.renderOpaque();
}
}
private void renderTranslucent() {
for (final Scene scene : this.scenes) {
scene.renderTranslucent();
}
}
public TextureMapper baseTextureMapper(final ModelInstance instance) {
final Model model = instance.model;
List<TextureMapper> mappers = this.textureMappers.get(model);
if (mappers == null) {
mappers = new ArrayList<>();
this.textureMappers.put(model, mappers);
}
if (mappers.isEmpty()) {
mappers.add(new TextureMapper(model));
}
return mappers.get(0);
}
public TextureMapper changeTextureMapper(final ModelInstance instance, final Object key, final Texture texture) {
final Map<Object, Texture> map = new HashMap<>(instance.textureMapper.textures);
if (texture instanceof Texture) { // not null?
map.put(key, texture);
}
else {
map.remove(key);
}
final Model model = instance.model;
final List<TextureMapper> mappers = this.textureMappers.get(model);
for (final TextureMapper mapper : mappers) {
if (mapper.textures.equals(map)) {
return mapper;
}
}
final TextureMapper mapper = new TextureMapper(model, map);
mappers.add(mapper);
return mapper;
}
}

View File

@ -0,0 +1,12 @@
package com.etheller.warsmash.viewer5;
public abstract class Resource {
public ModelViewer viewer;
public Resource(final ModelViewer viewer) {
this.viewer = viewer;
}
public abstract void bind(int unit);
}

View File

@ -0,0 +1,4 @@
package com.etheller.warsmash.viewer5;
public interface ResourceLoader {
}

View File

@ -0,0 +1,290 @@
package com.etheller.warsmash.viewer5;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.badlogic.gdx.math.Rectangle;
import com.etheller.warsmash.viewer5.handlers.Batch;
import com.etheller.warsmash.viewer5.handlers.BatchDescriptor;
/**
* A scene.
*
* Every scene has its own list of model instances, and its own camera and
* viewport.
*
* In addition, in Ghostwolf's original code every scene may have its own
* AudioContext if enableAudio() is called. If audo is enabled, the
* AudioContext's listener's location will be updated automatically. Note that
* due to browser policies, this may be done only after user interaction with
* the web page.
*
* In "Warsmash", we are starting from an attempt to replicate Ghostwolf, but
* audio is always on in LibGDX generally. So we will probably simplify or skip
* over those behaviors other than a boolean on/off toggle for audio.
*/
public class Scene {
private final ModelViewer viewer;
private final Camera camera;
private final Grid grid;
public int visibleCells;
public int visibleInstances;
public int updatedParticles;
private boolean audioEnabled;
private AudioContext audioContext;
private final List<ModelInstance> instances;
private final int currentInstance;
private final List<ModelInstance> batchedInstances;
private final int currentBatchedInstance;
public final EmittedObjectUpdater emitterObjectUpdater;
private final Map<TextureMapper, Batch> batches;
private final Comparator<ModelInstance> instanceDepthComparator;
public Scene(final ModelViewer viewer) {
final CanvasProvider canvas = viewer.canvas;
this.viewer = viewer;
this.camera = new Camera();
this.grid = new Grid(-100000, -100000, 200000, 200000, 200000, 200000);
this.visibleCells = 0;
this.visibleInstances = 0;
this.updatedParticles = 0;
this.audioEnabled = false;
this.audioContext = null;
// Use the whole canvas, and standard perspective projection values.
this.camera.viewport(new Rectangle(0, 0, canvas.getWidth(), canvas.getHeight()));
this.camera.perspective((float) (Math.PI / 4), canvas.getWidth() / canvas.getHeight(), 8, 10000);
this.instances = new ArrayList<>();
this.currentInstance = 0;
this.batchedInstances = new ArrayList<>();
this.currentBatchedInstance = 0;
this.emitterObjectUpdater = new EmittedObjectUpdater();
this.batches = new HashMap<>();
this.instanceDepthComparator = new InstanceDepthComparator();
}
public boolean enableAudio() {
if (this.audioContext == null) {
this.audioContext = new AudioContext();
}
if (!this.audioContext.isRunning()) {
this.audioContext.resume();
}
this.audioEnabled = this.audioContext.isRunning();
return this.audioEnabled;
}
public void disableAudio() {
if (this.audioContext != null) {
this.audioContext.suspend();
}
this.audioEnabled = false;
}
public boolean addInstance(final ModelInstance instance) {
if (instance.scene != this) {
if (instance.scene != null) {
instance.scene.removeInstance(instance);
}
instance.scene = this;
// Only allow instances that are actually ok to be added the scene.
if (instance.model.ok) {
this.grid.moved(instance);
return true;
}
}
return false;
}
public boolean removeInstance(final ModelInstance instance) {
if (instance.scene == this) {
this.grid.remove(instance);
instance.scene = null;
return true;
}
return false;
}
public void clear() {
// First remove references to this scene stored in the instances.
for (final GridCell cell : this.grid.cells) {
for (final ModelInstance instance : cell.instances) {
instance.scene = null;
}
}
// Then remove references to the instances.
this.grid.clear();
}
public boolean detach() {
if (this.viewer != null) {
return this.viewer.removeScene(this);
}
return false;
}
public void addToBatch(final ModelInstance instance) {
final TextureMapper textureMapper = instance.textureMapper;
Batch batch = this.batches.get(textureMapper);
if (batch == null) {
final Model model = instance.model;
final BatchDescriptor batchDescriptor = model.handler.batchDescriptor;
batch = batchDescriptor.create(this, model, textureMapper);
this.batches.put(textureMapper, batch);
}
batch.add(instance);
}
public void update(final float dt) {
this.camera.update();
if (this.audioEnabled) {
final float x = this.camera.location.x;
final float y = this.camera.location.y;
final float z = this.camera.location.z;
final float forwardX = this.camera.directionY.x;
final float forwardY = this.camera.directionY.y;
final float forwardZ = this.camera.directionY.z;
final float upX = this.camera.directionZ.x;
final float upY = this.camera.directionZ.y;
final float upZ = this.camera.directionZ.z;
final AudioContext.Listener listener = this.audioContext.listener;
listener.setPosition(-x, -y, -z);
listener.setOrientation(forwardX, forwardY, forwardZ, upX, upY, upZ);
}
final int frame = this.viewer.frame;
int currentInstance = 0;
int currentBatchedInstance = 0;
this.visibleCells = 0;
this.visibleInstances = 0;
// Update and collect all of the visible instances.
for (final GridCell cell : this.grid.cells) {
if (cell.isVisible(this.camera)) {
this.visibleCells += 1;
for (final ModelInstance instance : cell.instances) {
if (instance.rendered && (instance.cullFrame < frame) && instance.isVisible(this.camera)) {
instance.cullFrame = frame;
if (instance.updateFrame < frame) {
instance.update(dt, this);
}
if (instance.isBatched()) {
if (currentBatchedInstance < this.batchedInstances.size()) {
this.batchedInstances.set(currentBatchedInstance++, instance);
}
else {
this.batchedInstances.add(instance);
currentBatchedInstance++;
}
}
else {
if (currentInstance < this.instances.size()) {
this.instances.set(currentInstance++, instance);
}
else {
this.instances.add(instance);
currentInstance++;
}
}
this.visibleInstances += 1;
}
}
}
}
for (int i = this.batchedInstances.size() - 1; i >= currentBatchedInstance; i--) {
this.batchedInstances.remove(i);
}
for (int i = this.instances.size() - 1; i >= currentInstance; i--) {
this.instances.remove(i);
}
Collections.sort(this.instances, this.instanceDepthComparator);
this.emitterObjectUpdater.update(dt);
this.updatedParticles = this.emitterObjectUpdater.objects.size();
}
public void renderOpaque() {
final Rectangle viewport = this.camera.rect;
this.viewer.gl.glViewport((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height);
// Clear all of the batches.
for (final Batch batch : this.batches.values()) {
batch.clear();
}
// Add all of the batched instances to batches.
for (final ModelInstance instance : this.batchedInstances) {
this.addToBatch(instance);
}
// Render all of the batches.
for (final Batch batch : this.batches.values()) {
batch.render();
}
// Render all of the opaque things of non-batched instances.
for (final ModelInstance instance : this.instances) {
instance.renderOpaque();
}
}
/**
* Renders all translucent things in this scene. Automatically applies the
* camera's viewport.
*/
public void renderTranslucent() {
final Rectangle viewport = this.camera.rect;
this.viewer.gl.glViewport((int) viewport.x, (int) viewport.y, (int) viewport.width, (int) viewport.height);
for (final ModelInstance instance : this.instances) {
instance.renderTranslucent();
}
}
public void clearEmitterObjects() {
for (final EmittedObject object : this.emitterObjectUpdater.objects) {
object.health = 0;
}
}
private static final class InstanceDepthComparator implements Comparator<ModelInstance> {
@Override
public int compare(final ModelInstance o1, final ModelInstance o2) {
return o2.depth - o1.depth;
}
}
}

View File

@ -0,0 +1,25 @@
package com.etheller.warsmash.viewer5;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.graphics.Texture;
public class TextureMapper {
public final Model model;
public final Map<Object, Texture> textures;
public TextureMapper(final Model model) {
this.model = model;
this.textures = new HashMap<>();
}
public TextureMapper(final Model model, final Map<Object, Texture> textures) {
this.model = model;
this.textures = new HashMap<>(textures);
}
public Texture get(final Object key) {
return this.textures.get(key);
}
}

View File

@ -0,0 +1,15 @@
package com.etheller.warsmash.viewer5.deprecated;
import com.etheller.warsmash.viewer5.gl.WebGL;
public class ShaderProgram {
public boolean ok;
public int attribsCount;
public int webglResource;
public ShaderProgram(final WebGL webGL, final ShaderUnitDeprecated vertexShader, final ShaderUnitDeprecated fragmentShader) {
// TODO Auto-generated constructor stub
}
}

View File

@ -0,0 +1,31 @@
package com.etheller.warsmash.viewer5.deprecated;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import com.badlogic.gdx.graphics.GL20;
public class ShaderUnitDeprecated {
public boolean ok;
private final int webglResource;
private final String src;
private final int shaderType;
public ShaderUnitDeprecated(final GL20 gl, final String src, final int type) {
final int id = gl.glCreateShader(type);
this.ok = false;
this.webglResource = id;
this.src = src;
this.shaderType = type;
gl.glShaderSource(id, src);
gl.glCompileShader(id);
final IntBuffer success = ByteBuffer.allocateDirect(8).order(ByteOrder.nativeOrder()).asIntBuffer();
gl.glGetShaderiv(id, GL20.GL_COMPILE_STATUS, success);
throw new UnsupportedOperationException("Not yet implemented, probably using library instead");
}
}

View File

@ -0,0 +1,139 @@
package com.etheller.warsmash.viewer5.gl;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.etheller.warsmash.viewer5.deprecated.ShaderUnitDeprecated;
/**
* This needs a rename. Just a ripoff of ghostwolf's wrapper utility class, it's
* a utility, not a webgl
*/
public class WebGL {
public GL20 gl;
public Map<Integer, ShaderUnitDeprecated> shaderUnits;
public Map<Integer, ShaderProgram> shaderPrograms;
public ShaderProgram currentShaderProgram;
public String floatPrecision;
public final Texture emptyTexture;
public WebGL(final GL20 gl) {
gl.glDepthFunc(GL20.GL_LEQUAL);
gl.glEnable(GL20.GL_DEPTH_TEST);
// TODO here ghostwolf throws exceptions for unsupported versions of opengl
this.gl = gl;
this.shaderUnits = new HashMap<>();
this.shaderPrograms = new HashMap<>();
this.currentShaderProgram = null;
this.floatPrecision = "precision mediump float;\n";
final Pixmap imageData = new Pixmap(2, 2, Pixmap.Format.RGBA8888);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
imageData.drawPixel(i, j, 0x000000FF);
}
}
this.emptyTexture = new Texture(imageData);
}
public ShaderUnitDeprecated createShaderUnit(final String src, final int type) {
final int hash = stringHash(src); // TODO: why on earth are we doing this, what about hash collisions?
if (!this.shaderUnits.containsKey(hash)) {
this.shaderUnits.put(hash, new ShaderUnitDeprecated(this.gl, src, type));
}
return this.shaderUnits.get(hash);
}
public ShaderProgram createShaderProgram(final String vertexSrc, final String fragmentSrc) {
final Map<Integer, ShaderProgram> shaderPrograms = this.shaderPrograms;
final int hash = stringHash(vertexSrc + fragmentSrc);
if (!shaderPrograms.containsKey(hash)) {
shaderPrograms.put(hash, new ShaderProgram(vertexSrc, fragmentSrc));
}
final ShaderProgram shaderProgram = shaderPrograms.get(hash);
if (shaderProgram.isCompiled()) {
return shaderProgram;
}
return null;
}
public void enableVertexAttribs(final int start, final int end) {
final GL20 gl = this.gl;
for (int i = start; i < end; i++) {
gl.glEnableVertexAttribArray(i);
}
}
public void disableVertexAttribs(final int start, final int end) {
final GL20 gl = this.gl;
for (int i = start; i < end; i++) {
gl.glDisableVertexAttribArray(i);
}
}
public void useShaderProgram(final ShaderProgram shaderProgram) {
final ShaderProgram currentShaderProgram = this.currentShaderProgram;
if ((shaderProgram != null) && shaderProgram.isCompiled() && (shaderProgram != currentShaderProgram)) {
int oldAttribs = 0;
final int newAttribs = shaderProgram.getAttributes().length;
if (currentShaderProgram != null) {
oldAttribs = currentShaderProgram.getAttributes().length;
}
shaderProgram.begin();
if (newAttribs > oldAttribs) {
this.enableVertexAttribs(oldAttribs, newAttribs);
}
else if (newAttribs < oldAttribs) {
this.disableVertexAttribs(newAttribs, oldAttribs);
}
this.currentShaderProgram = shaderProgram;
}
}
public void bindTexture(final Texture texture, final int unit) {
final GL20 gl = this.gl;
gl.glActiveTexture(GL20.GL_TEXTURE0 + unit);
if (texture != null /* && texture.ok */) {
texture.bind();
}
else {
this.emptyTexture.bind();
}
}
public void setTextureMode(final int wrapS, final int wrapT, final int magFilter, final int minFilter) {
final GL20 gl = this.gl;
// TODO make sure we dont assign this parameter doubly if we're already using
// libgdx texture, which does do some wrapS and wrapT stuff already
gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_S, wrapS);
gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_WRAP_T, wrapT);
gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MAG_FILTER, magFilter);
gl.glTexParameteri(GL20.GL_TEXTURE_2D, GL20.GL_TEXTURE_MIN_FILTER, minFilter);
}
private int stringHash(final String src) {
return src.hashCode();
}
}

View File

@ -0,0 +1,22 @@
package com.etheller.warsmash.viewer5.handlers;
import com.etheller.warsmash.viewer5.ModelInstance;
public class Batch {
public void add(final ModelInstance instance) {
// TODO Auto-generated method stub
}
public void clear() {
// TODO Auto-generated method stub
}
public void render() {
// TODO Auto-generated method stub
}
}

View File

@ -0,0 +1,9 @@
package com.etheller.warsmash.viewer5.handlers;
import com.etheller.warsmash.viewer5.Model;
import com.etheller.warsmash.viewer5.Scene;
import com.etheller.warsmash.viewer5.TextureMapper;
public interface BatchDescriptor {
Batch create(Scene scene, Model model, TextureMapper textureMapper);
}

View File

@ -0,0 +1,5 @@
package com.etheller.warsmash.viewer5.handlers;
public class EmitterObject {
}

View File

@ -0,0 +1,5 @@
package com.etheller.warsmash.viewer5.handlers;
public class Handler {
public BatchDescriptor batchDescriptor;
}