Commit c06d3a89 authored by Luna Riegel's avatar Luna Riegel
Browse files

Merge branch 'dev' into dev_topo_checks

# Conflicts:
#	CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/HighlightController.java
#	CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/ListErrorVisitor.java
#	CityDoctorParent/Extensions/CityDoctorGUI/src/main/java/de/hft/stuttgart/citydoctor2/gui/Renderer.java
parents 15585c6e cc4fe260
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL3;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/** Off-screen framebuffer that stores picking ids as RGBA8; supports single-pixel readback. */
public class PickingFramebuffer {
private int fbo;
private int colorTex;
private int depthRbo;
private int width;
private int height;
private boolean created;
public void resize(GL3 gl, int w, int h) {
if (w <= 0 || h <= 0 || (w == width && h == height && created)) {
return;
}
dispose(gl);
width = w;
height = h;
int[] tmp = new int[1];
gl.glGenFramebuffers(1, tmp, 0);
fbo = tmp[0];
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, fbo);
gl.glGenTextures(1, tmp, 0);
colorTex = tmp[0];
gl.glBindTexture(GL.GL_TEXTURE_2D, colorTex);
gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA8, w, h, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, null);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
gl.glFramebufferTexture2D(GL.GL_FRAMEBUFFER, GL.GL_COLOR_ATTACHMENT0, GL.GL_TEXTURE_2D, colorTex, 0);
gl.glGenRenderbuffers(1, tmp, 0);
depthRbo = tmp[0];
gl.glBindRenderbuffer(GL.GL_RENDERBUFFER, depthRbo);
gl.glRenderbufferStorage(GL.GL_RENDERBUFFER, GL.GL_DEPTH_COMPONENT16, w, h);
gl.glFramebufferRenderbuffer(GL.GL_FRAMEBUFFER, GL.GL_DEPTH_ATTACHMENT, GL.GL_RENDERBUFFER, depthRbo);
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);
created = true;
}
public void bind(GL3 gl) {
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, fbo);
gl.glViewport(0, 0, width, height);
}
/**
* Reads the picking id at canvas pixel (x, y) measured from the top-left. GL's origin is bottom-left,
* so the row is flipped.
*/
public int readId(GL3 gl, int x, int y) {
if (!created || x < 0 || y < 0 || x >= width || y >= height) {
return 0;
}
ByteBuffer buf = ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder());
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, fbo);
gl.glReadPixels(x, height - 1 - y, 1, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buf);
gl.glBindFramebuffer(GL.GL_FRAMEBUFFER, 0);
int r = buf.get(0) & 0xFF;
int g = buf.get(1) & 0xFF;
int b = buf.get(2) & 0xFF;
return PickingCodec.decode(r, g, b);
}
public void dispose(GL3 gl) {
if (created) {
gl.glDeleteFramebuffers(1, new int[]{fbo}, 0);
gl.glDeleteTextures(1, new int[]{colorTex}, 0);
gl.glDeleteRenderbuffers(1, new int[]{depthRbo}, 0);
created = false;
}
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurfaceType;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;
import de.hft.stuttgart.citydoctor2.utils.visitors.CityObjectCollector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
import javafx.scene.paint.Color;
/**
* Builds {@link SceneData} from model polygons: tesselate, bake shade colour, assign picking ids,
* accumulate vertices and per-triangle metadata, recentre by the bounding-box centre.
* Intended to run on a worker thread.
*/
public final class SceneBuilder {
private SceneBuilder() {}
public static SceneData fromPolygons(Collection<? extends Polygon> polygons, Color baseColor) {
return build(polygons, baseColor, Lod.LOD2.ordinal(), false);
}
/** General entry: caller supplies polygons already chosen, plus their lod/roof attributes. */
public static SceneData build(Collection<? extends Polygon> polygons, Color baseColor,
int lod, boolean roof) {
Vector3d center = findCenter(polygons);
MeshAccumulator acc = new MeshAccumulator(center);
PickRegistry registry = new PickRegistry();
List<TriangleMeta> metas = new ArrayList<>();
int triangleIndex = 0;
for (Polygon p : polygons) {
Color base = chooseBase(p, baseColor);
Color shaded = ShadeColor.shade(p.calculateNormalNormalized(), base);
float r = (float) shaded.getRed();
float g = (float) shaded.getGreen();
float b = (float) shaded.getBlue();
int id = registry.register(p);
TesselatedPolygon tp = p.tesselate();
for (Triangle3d t : tp.getTriangles()) {
acc.addTriangle(t, id, r, g, b);
metas.add(new TriangleMeta(triangleIndex++, p, lod, roof));
}
}
double[] centerArr = {center.getX(), center.getY(), center.getZ()};
return new SceneData(acc.positions(), acc.colors(), acc.ids(), metas, registry, centerArr);
}
/**
* Builds ALL features of the model into a single {@link SceneData} with one shared center,
* one {@link PickRegistry} and continuous triangle indices. Filtering by LOD/roof happens later
* via the index set, not at build time. Replicates the per-feature base colours of the legacy
* {@code TriangulatedGeometry.of(model, filters)}.
*/
public static SceneData fromModel(CityDoctorModel model) {
Vector3d center = findCenterOfModel(model);
MeshAccumulator acc = new MeshAccumulator(center);
PickRegistry registry = new PickRegistry();
List<TriangleMeta> metas = new ArrayList<>();
int[] triangleIndex = {0};
appendFeatures(model.getBuildings(), Color.WHITE, acc, registry, metas, triangleIndex);
appendFeatures(model.getBridges(), Color.LIGHTSTEELBLUE, acc, registry, metas, triangleIndex);
appendFeatures(model.getTunnels(), Color.SLATEGRAY, acc, registry, metas, triangleIndex);
appendFeatures(model.getLand(), Color.TAN, acc, registry, metas, triangleIndex);
appendFeatures(model.getTransportation(), Color.DIMGRAY, acc, registry, metas, triangleIndex);
appendFeatures(model.getVegetation(), Color.LIGHTGREEN, acc, registry, metas, triangleIndex);
appendFeatures(model.getWater(), Color.LIGHTSKYBLUE, acc, registry, metas, triangleIndex);
appendFeatures(model.getCityFurniture(), Color.BLUEVIOLET, acc, registry, metas, triangleIndex);
appendFeatures(model.getOtherCityObjects(), Color.BLACK, acc, registry, metas, triangleIndex);
double[] centerArr = {center.getX(), center.getY(), center.getZ()};
return new SceneData(acc.positions(), acc.colors(), acc.ids(), metas, registry, centerArr);
}
private static void appendFeatures(Stream<? extends CityObject> cos, Color color,
MeshAccumulator acc, PickRegistry registry,
List<TriangleMeta> metas, int[] triangleIndex) {
CityObjectCollector collector = new CityObjectCollector();
cos.forEach(cityObject -> cityObject.accept(collector));
for (CityObject co : collector.getCityObjects()) {
for (Geometry geom : co.getGeometries()) {
int lod = geom.getLod() == null ? Lod.LOD2.ordinal() : geom.getLod().ordinal();
for (Polygon p : geom.getPolygons()) {
if (p.isLink()) {
continue;
}
boolean roof = p.getPartOfSurface() != null
&& p.getPartOfSurface().getType() == BoundarySurfaceType.ROOF;
appendPolygon(p, color, lod, roof, acc, registry, metas, triangleIndex);
}
}
}
}
private static void appendPolygon(Polygon p, Color baseColor, int lod, boolean roof,
MeshAccumulator acc, PickRegistry registry,
List<TriangleMeta> metas, int[] triangleIndex) {
Color base = chooseBase(p, baseColor);
Color shaded = ShadeColor.shade(p.calculateNormalNormalized(), base);
float r = (float) shaded.getRed();
float g = (float) shaded.getGreen();
float b = (float) shaded.getBlue();
int id = registry.register(p);
TesselatedPolygon tp = p.tesselate();
for (Triangle3d t : tp.getTriangles()) {
acc.addTriangle(t, id, r, g, b);
metas.add(new TriangleMeta(triangleIndex[0]++, p, lod, roof));
}
}
private static Vector3d findCenterOfModel(CityDoctorModel model) {
double xMin = Double.MAX_VALUE, yMin = Double.MAX_VALUE, zMin = Double.MAX_VALUE;
double xMax = -Double.MAX_VALUE, yMax = -Double.MAX_VALUE, zMax = -Double.MAX_VALUE;
boolean any = false;
for (CityObject co : model.createFeatureStream().toList()) {
CityObjectCollector collector = new CityObjectCollector();
co.accept(collector);
for (CityObject inner : collector.getCityObjects()) {
for (Geometry geom : inner.getGeometries()) {
for (Polygon p : geom.getPolygons()) {
if (p.isLink()) {
continue;
}
for (Vector3d v : p.getExteriorRing().getVertices()) {
xMin = Math.min(xMin, v.getX()); xMax = Math.max(xMax, v.getX());
yMin = Math.min(yMin, v.getY()); yMax = Math.max(yMax, v.getY());
zMin = Math.min(zMin, v.getZ()); zMax = Math.max(zMax, v.getZ());
any = true;
}
}
}
}
}
if (!any) {
return new Vector3d(0, 0, 0);
}
return new Vector3d((xMax - xMin) / 2 + xMin, (yMax - yMin) / 2 + yMin, (zMax - zMin) / 2 + zMin);
}
private static Color chooseBase(Polygon p, Color fallback) {
Color rc = p.getRenderColor();
if (rc != null && !rc.equals(Color.WHITE)) {
return rc;
}
return fallback;
}
private static Vector3d findCenter(Collection<? extends Polygon> polygons) {
double xMin = Double.MAX_VALUE, yMin = Double.MAX_VALUE, zMin = Double.MAX_VALUE;
double xMax = -Double.MAX_VALUE, yMax = -Double.MAX_VALUE, zMax = -Double.MAX_VALUE;
for (Polygon p : polygons) {
for (Vector3d v : p.getExteriorRing().getVertices()) {
xMin = Math.min(xMin, v.getX()); xMax = Math.max(xMax, v.getX());
yMin = Math.min(yMin, v.getY()); yMax = Math.max(yMax, v.getY());
zMin = Math.min(zMin, v.getZ()); zMax = Math.max(zMax, v.getZ());
}
}
if (xMin > xMax) {
return new Vector3d(0, 0, 0);
}
return new Vector3d((xMax - xMin) / 2 + xMin, (yMax - yMin) / 2 + yMin, (zMax - zMin) / 2 + zMin);
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.List;
/** Immutable CPU-side result of building the scene; uploaded to the GPU by the GL layer. */
public class SceneData {
private final float[] positions;
private final float[] colors;
private final int[] ids;
private final List<TriangleMeta> triangleMetas;
private final PickRegistry pickRegistry;
private final double[] center;
public SceneData(float[] positions, float[] colors, int[] ids,
List<TriangleMeta> triangleMetas, PickRegistry pickRegistry, double[] center) {
this.positions = positions;
this.colors = colors;
this.ids = ids;
this.triangleMetas = triangleMetas;
this.pickRegistry = pickRegistry;
this.center = center;
}
public float[] positions() { return positions; }
public float[] colors() { return colors; }
public int[] ids() { return ids; }
public List<TriangleMeta> triangleMetas() { return triangleMetas; }
public PickRegistry pickRegistry() { return pickRegistry; }
public double[] center() { return center; }
public int triangleCount() { return triangleMetas.size(); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import de.hft.stuttgart.citydoctor2.math.UnitVector3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import javafx.scene.paint.Color;
/**
* Computes the surface-differentiation shade used by the legacy renderer: brightness derived from the
* angle between the polygon normal and a fixed axis. Not physical lighting; only makes adjacent faces
* distinguishable. Ported from {@code TriangulatedGeometry.calculateMaterial}.
*/
public final class ShadeColor {
private static final UnitVector3d AXIS = new Vector3d(19, 0.8, 1.5).normalize();
private ShadeColor() {}
public static Color shade(Vector3d normal, Color base) {
double cos = normal.dot(AXIS);
// clamp to acos domain; raw dot can drift slightly outside [-1,1] for near-parallel normals
cos = Math.max(-1.0, Math.min(1.0, cos));
double brightness = Math.acos(cos) / Math.PI * 0.6 + 0.3; // -> [0.3, 0.9]
return base.deriveColor(0, 1.0, brightness, 1.0);
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import com.jogamp.opengl.GL3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
/** Compiles and links a GLSL program from classpath resources next to this class. */
public class ShaderProgram {
private final int program;
public ShaderProgram(GL3 gl, String vertexResource, String fragmentResource) {
this(gl, vertexResource, null, fragmentResource);
}
/** Builds a program with an optional geometry stage ({@code geometryResource} may be null). */
public ShaderProgram(GL3 gl, String vertexResource, String geometryResource, String fragmentResource) {
int vs = compile(gl, GL3.GL_VERTEX_SHADER, read(vertexResource));
int gs = geometryResource != null ? compile(gl, GL3.GL_GEOMETRY_SHADER, read(geometryResource)) : 0;
int fs = compile(gl, GL3.GL_FRAGMENT_SHADER, read(fragmentResource));
program = gl.glCreateProgram();
gl.glAttachShader(program, vs);
if (gs != 0) {
gl.glAttachShader(program, gs);
}
gl.glAttachShader(program, fs);
gl.glLinkProgram(program);
checkLink(gl, program);
gl.glDeleteShader(vs);
if (gs != 0) {
gl.glDeleteShader(gs);
}
gl.glDeleteShader(fs);
}
public void use(GL3 gl) { gl.glUseProgram(program); }
public int uniform(GL3 gl, String name) { return gl.glGetUniformLocation(program, name); }
public void dispose(GL3 gl) { gl.glDeleteProgram(program); }
private static int compile(GL3 gl, int type, String src) {
int shader = gl.glCreateShader(type);
gl.glShaderSource(shader, 1, new String[]{src}, new int[]{src.length()}, 0);
gl.glCompileShader(shader);
int[] status = new int[1];
gl.glGetShaderiv(shader, GL3.GL_COMPILE_STATUS, status, 0);
if (status[0] == 0) {
throw new IllegalStateException("Shader compile failed: " + infoLog(gl, shader, true));
}
return shader;
}
private static void checkLink(GL3 gl, int program) {
int[] status = new int[1];
gl.glGetProgramiv(program, GL3.GL_LINK_STATUS, status, 0);
if (status[0] == 0) {
throw new IllegalStateException("Program link failed: " + infoLog(gl, program, false));
}
}
private static String infoLog(GL3 gl, int handle, boolean shader) {
int[] len = new int[1];
if (shader) {
gl.glGetShaderiv(handle, GL3.GL_INFO_LOG_LENGTH, len, 0);
} else {
gl.glGetProgramiv(handle, GL3.GL_INFO_LOG_LENGTH, len, 0);
}
byte[] log = new byte[Math.max(1, len[0])];
if (shader) {
gl.glGetShaderInfoLog(handle, log.length, new int[1], 0, log, 0);
} else {
gl.glGetProgramInfoLog(handle, log.length, new int[1], 0, log, 0);
}
return new String(log, StandardCharsets.UTF_8);
}
private static String read(String resource) {
try (InputStream in = ShaderProgram.class.getResourceAsStream(resource)) {
if (in == null) {
throw new IllegalStateException("Shader resource not found: " + resource);
}
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
return r.lines().collect(Collectors.joining("\n"));
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
/**
* Metadata for one triangle in the global buffer.
* @param triangleIndex index of this triangle (vertices are 3*triangleIndex .. +2)
* @param polygon source polygon (for picking/highlight resolution); may be null in tests
* @param lod LOD ordinal (Lod.ordinal()) of the owning geometry
* @param roof true if the triangle belongs to a ROOF boundary surface
*/
public record TriangleMeta(int triangleIndex, Polygon polygon, int lod, boolean roof) {}
#version 330 core
flat in int vId;
out vec4 fragColor;
void main() {
int id = vId;
float r = float( id & 0xFF) / 255.0;
float g = float((id >> 8) & 0xFF) / 255.0;
float b = float((id >> 16) & 0xFF) / 255.0;
fragColor = vec4(r, g, b, 1.0);
}
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 2) in int aId;
uniform mat4 uMVP;
flat out int vId;
void main() {
gl_Position = uMVP * vec4(aPos, 1.0);
vId = aId;
}
#version 330 core
in vec2 vQuad;
in vec3 vColor;
out vec4 fragColor;
void main() {
float r2 = dot(vQuad, vQuad);
if (r2 > 1.0) {
discard; // outside the sphere's silhouette
}
fragColor = vec4(vColor, 1.0);
}
#version 330 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
in vec3 vColorG[];
uniform vec2 uViewport; // framebuffer size in pixels
uniform float uPointSize; // sphere diameter in pixels
out vec2 vQuad; // billboard-local coords in [-1, 1]
out vec3 vColor;
void main() {
vec4 c = gl_in[0].gl_Position;
if (c.w <= 0.0) {
return; // behind the camera
}
// half-extent in clip space for a constant on-screen pixel size
vec2 h = vec2(uPointSize / uViewport.x, uPointSize / uViewport.y) * c.w;
vColor = vColorG[0];
gl_Position = c + vec4(-h.x, -h.y, 0.0, 0.0); vQuad = vec2(-1.0, -1.0); EmitVertex();
gl_Position = c + vec4( h.x, -h.y, 0.0, 0.0); vQuad = vec2( 1.0, -1.0); EmitVertex();
gl_Position = c + vec4(-h.x, h.y, 0.0, 0.0); vQuad = vec2(-1.0, 1.0); EmitVertex();
gl_Position = c + vec4( h.x, h.y, 0.0, 0.0); vQuad = vec2( 1.0, 1.0); EmitVertex();
EndPrimitive();
}
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
uniform mat4 uMVP;
out vec3 vColorG;
void main() {
gl_Position = uMVP * vec4(aPos, 1.0);
vColorG = aColor;
}
#version 330 core
in vec3 vColor;
out vec4 fragColor;
void main() {
fragColor = vec4(vColor, 1.0);
}
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
uniform mat4 uMVP;
out vec3 vColor;
void main() {
gl_Position = uMVP * vec4(aPos, 1.0);
vColor = aColor;
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class FloatListTest {
@Test
public void growsBeyondInitialCapacityAndPreservesOrder() {
FloatList list = new FloatList(2);
for (int i = 0; i < 10; i++) {
list.add(i);
}
assertEquals(10, list.size());
float[] expected = {0,1,2,3,4,5,6,7,8,9};
assertArrayEquals(expected, list.toArray(), 0.0f);
}
@Test
public void addThreeAddsAllInOrder() {
FloatList list = new FloatList(1);
list.add(1f, 2f, 3f);
assertArrayEquals(new float[]{1,2,3}, list.toArray(), 0.0f);
}
@Test
public void clearResetsSizeAndArray() {
FloatList list = new FloatList(4);
list.add(1f, 2f, 3f);
list.add(4f);
list.clear();
assertEquals(0, list.size());
assertEquals(0, list.toArray().length);
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertArrayEquals;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import org.junit.Test;
public class IndexSetBuilderTest {
@Test
public void includesOnlyTrianglesMatchingThePredicate() {
List<TriangleMeta> metas = Arrays.asList(
new TriangleMeta(0, null, 1, false),
new TriangleMeta(1, null, 2, false),
new TriangleMeta(2, null, 1, false));
Predicate<TriangleMeta> onlyGroup1 = m -> m.lod() == 1;
int[] indices = IndexSetBuilder.build(metas, onlyGroup1);
assertArrayEquals(new int[]{0, 1, 2, 6, 7, 8}, indices);
}
@Test
public void emptyWhenNothingMatches() {
List<TriangleMeta> metas = Arrays.asList(new TriangleMeta(0, null, 1, false));
int[] indices = IndexSetBuilder.build(metas, m -> false);
assertArrayEquals(new int[]{}, indices);
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class Mat4Test {
@Test
public void identityTimesVectorIsUnchanged() {
float[] v = Mat4.transform(Mat4.identity(), 2, 3, 4, 1);
assertArrayEquals(new float[]{2, 3, 4, 1}, v, 1e-6f);
}
@Test
public void translationMovesPoint() {
float[] m = Mat4.translation(5, -2, 1);
float[] v = Mat4.transform(m, 1, 1, 1, 1);
assertArrayEquals(new float[]{6, -1, 2, 1}, v, 1e-6f);
}
@Test
public void multiplyIsIdentityNeutral() {
float[] t = Mat4.translation(1, 2, 3);
assertArrayEquals(t, Mat4.multiply(Mat4.identity(), t), 1e-6f);
assertArrayEquals(t, Mat4.multiply(t, Mat4.identity()), 1e-6f);
}
@Test
public void rotationZ90MapsXAxisToYAxis() {
float[] m = Mat4.rotationZ((float) (Math.PI / 2));
float[] v = Mat4.transform(m, 1, 0, 0, 1);
assertEquals(0f, v[0], 1e-6f);
assertEquals(1f, v[1], 1e-6f);
}
@Test
public void perspectiveHasExpectedDiagonalStructure() {
float[] p = Mat4.perspective((float) Math.toRadians(60), 1.0f, 1f, 100f);
float f = (float) (1.0 / Math.tan(Math.toRadians(60) / 2));
assertEquals(f, p[0], 1e-5f);
assertEquals(f, p[5], 1e-5f);
assertEquals(-1f, p[11], 1e-6f);
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import org.junit.Test;
public class MeshAccumulatorTest {
@Test
public void emitsNineFloatsAndThreeIdsPerTriangleRecentered() {
MeshAccumulator acc = new MeshAccumulator(new Vector3d(1, 0, 0));
Triangle3d t = new Triangle3d(
new Vector3d(1, 0, 0), new Vector3d(2, 0, 0), new Vector3d(1, 1, 0));
acc.addTriangle(t, 7, 0.5f, 0.25f, 0.125f);
assertEquals(3, acc.vertexCount());
assertArrayEquals(new float[]{0,0,0, 1,0,0, 0,1,0}, acc.positions(), 1e-6f);
assertArrayEquals(new float[]{0.5f,0.25f,0.125f, 0.5f,0.25f,0.125f, 0.5f,0.25f,0.125f},
acc.colors(), 1e-6f);
assertArrayEquals(new int[]{7, 7, 7}, acc.ids());
}
@Test
public void accumulatesMultipleTriangles() {
MeshAccumulator acc = new MeshAccumulator(new Vector3d(0, 0, 0));
Triangle3d t = new Triangle3d(
new Vector3d(0, 0, 0), new Vector3d(1, 0, 0), new Vector3d(0, 1, 0));
acc.addTriangle(t, 1, 1, 1, 1);
acc.addTriangle(t, 2, 0, 0, 0);
assertEquals(6, acc.vertexCount());
assertEquals(18, acc.positions().length);
assertArrayEquals(new int[]{1, 1, 1, 2, 2, 2}, acc.ids());
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class OrbitCameraTest {
private static float[] ndc(OrbitCamera cam, float x, float y, float z) {
float[] clip = Mat4.transform(cam.viewProjection(), x, y, z, 1);
return new float[]{clip[0] / clip[3], clip[1] / clip[3], clip[2] / clip[3]};
}
@Test
public void focusPointProjectsToScreenCentre() {
OrbitCamera cam = new OrbitCamera();
cam.setAspect(1.0f);
cam.zoomOutForBoundingBox(50.0);
float[] p = ndc(cam, 0, 0, 0);
assertEquals(0f, p[0], 1e-4f);
assertEquals(0f, p[1], 1e-4f);
assertTrue("origin in front of camera (within clip)", p[2] > -1f && p[2] < 1f);
}
@Test
public void zoomingKeepsFocusCentredButChangesDepth() {
OrbitCamera cam = new OrbitCamera();
cam.setAspect(1.0f);
cam.zoomOutForBoundingBox(50.0);
float depthBefore = ndc(cam, 0, 0, 0)[2];
cam.zoom(1.5);
float[] p = ndc(cam, 0, 0, 0);
assertEquals(0f, p[0], 1e-4f);
assertEquals(0f, p[1], 1e-4f);
assertTrue("depth changes when zooming", Math.abs(p[2] - depthBefore) > 1e-4f);
}
@Test
public void resetRestoresInitialDistanceAndAngles() {
OrbitCamera cam = new OrbitCamera();
cam.setAspect(1.0f);
cam.orbit(40, 25);
cam.pan(10, -5);
cam.zoom(2.0);
cam.reset();
float[] before = cam.viewProjection();
OrbitCamera fresh = new OrbitCamera();
fresh.setAspect(1.0f);
org.junit.Assert.assertArrayEquals(fresh.viewProjection(), before, 1e-5f);
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Test;
public class PickRegistryTest {
@Test
public void assignsSequentialNonZeroIdsAndResolvesThem() {
PickRegistry reg = new PickRegistry();
Object a = "a";
Object b = "b";
int idA = reg.register(a);
int idB = reg.register(b);
assertEquals(1, idA);
assertEquals(2, idB);
assertEquals(a, reg.resolve(idA));
assertEquals(b, reg.resolve(idB));
}
@Test
public void resolvesBackgroundAndOutOfRangeToNull() {
PickRegistry reg = new PickRegistry();
reg.register("a");
assertNull(reg.resolve(0));
assertNull(reg.resolve(99));
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class PickingCodecTest {
@Test
public void roundTripsIdsThroughRgbBytes() {
int[] ids = {1, 255, 256, 65535, 65536, 16777215};
for (int id : ids) {
int r = PickingCodec.red(id);
int g = PickingCodec.green(id);
int b = PickingCodec.blue(id);
assertEquals(id, PickingCodec.decode(r, g, b));
}
}
@Test
public void blackDecodesToZeroBackground() {
assertEquals(0, PickingCodec.decode(0, 0, 0));
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment