Commit 854aab9b authored by Matthias Betz's avatar Matthias Betz
Browse files

Merge remote-tracking branch 'remotes/origin/dev' into 120-portierung-sweep-algorithmen

parents 760147a4 cc4fe260
Pipeline #12442 passed with stage
in 2 minutes and 26 seconds
package de.hft.stuttgart.citydoctor2.gui.gl;
/**
* Minimal column-major 4x4 float matrix helpers, layout-compatible with OpenGL uniform uploads
* (element index = column*4 + row).
*/
public final class Mat4 {
private Mat4() {}
public static float[] identity() {
float[] m = new float[16];
m[0] = m[5] = m[10] = m[15] = 1f;
return m;
}
public static float[] translation(float x, float y, float z) {
float[] m = identity();
m[12] = x;
m[13] = y;
m[14] = z;
return m;
}
/** Returns a * b (column-major). */
public static float[] multiply(float[] a, float[] b) {
float[] r = new float[16];
for (int col = 0; col < 4; col++) {
for (int row = 0; row < 4; row++) {
float sum = 0f;
for (int k = 0; k < 4; k++) {
sum += a[k * 4 + row] * b[col * 4 + k];
}
r[col * 4 + row] = sum;
}
}
return r;
}
/** Returns m * (x,y,z,w) as a length-4 array. */
public static float[] transform(float[] m, float x, float y, float z, float w) {
return new float[] {
m[0] * x + m[4] * y + m[8] * z + m[12] * w,
m[1] * x + m[5] * y + m[9] * z + m[13] * w,
m[2] * x + m[6] * y + m[10] * z + m[14] * w,
m[3] * x + m[7] * y + m[11] * z + m[15] * w
};
}
public static float[] rotationX(float rad) {
float c = (float) Math.cos(rad);
float s = (float) Math.sin(rad);
float[] m = identity();
m[5] = c; m[9] = -s;
m[6] = s; m[10] = c;
return m;
}
public static float[] rotationZ(float rad) {
float c = (float) Math.cos(rad);
float s = (float) Math.sin(rad);
float[] m = identity();
m[0] = c; m[4] = -s;
m[1] = s; m[5] = c;
return m;
}
/** Right-handed perspective projection (camera looks down -Z). */
public static float[] perspective(float fovyRad, float aspect, float near, float far) {
float f = (float) (1.0 / Math.tan(fovyRad / 2.0));
float[] m = new float[16];
m[0] = f / aspect;
m[5] = f;
m[10] = (far + near) / (near - far);
m[11] = -1f;
m[14] = (2f * far * near) / (near - far);
return m;
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
/**
* Builds interleaved-by-attribute vertex arrays from triangles. Vertices are stored per triangle corner
* (3 per triangle) so a triangle's vertices occupy a contiguous, predictable range — which lets the
* draw index set be built by triangle without dedup bookkeeping.
*/
public class MeshAccumulator {
private final Vector3d center;
private final FloatList positions = new FloatList();
private final FloatList colors = new FloatList();
private final IntList ids = new IntList();
private int vertexCount;
public MeshAccumulator(Vector3d center) {
this.center = center;
}
public void addTriangle(Triangle3d t, int id, float r, float g, float b) {
addVertex(t.getP1(), id, r, g, b);
addVertex(t.getP2(), id, r, g, b);
addVertex(t.getP3(), id, r, g, b);
}
private void addVertex(Vector3d p, int id, float r, float g, float b) {
positions.add((float) (p.getX() - center.getX()),
(float) (p.getY() - center.getY()),
(float) (p.getZ() - center.getZ()));
colors.add(r, g, b);
ids.add(id);
vertexCount++;
}
public int vertexCount() { return vertexCount; }
public float[] positions() { return positions.toArray(); }
public float[] colors() { return colors.toArray(); }
public int[] ids() { return ids.toArray(); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
/**
* Orbit camera reproducing the legacy JavaFX interaction model: world is rotated about Z then X, the
* camera sits at {@code distance} along -Z (negative distance = farther away) and pans in X/Y.
*/
public class OrbitCamera {
private static final double INITIAL_AZIMUTH = 180.0;
private static final double INITIAL_ELEVATION = -60.0;
private static final double INITIAL_DISTANCE = 100.0;
private static final double FOVY_RAD = Math.toRadians(30);
private static final float NEAR = 0.1f;
private static final float FAR = 10000f;
private double azimuthDeg = INITIAL_AZIMUTH;
private double elevationDeg = INITIAL_ELEVATION;
private double distance = INITIAL_DISTANCE;
private double panX = 0;
private double panY = 0;
private float aspect = 1.0f;
public void setAspect(float aspect) {
if (aspect > 0) {
this.aspect = aspect;
}
}
public void orbit(double deltaAzimuthDeg, double deltaElevationDeg) {
azimuthDeg += deltaAzimuthDeg;
elevationDeg += deltaElevationDeg;
}
public void pan(double dx, double dy) {
panX += dx;
panY += dy;
}
/** factor > 1 moves farther, < 1 moves closer. */
public void zoom(double factor) {
distance *= factor;
}
/** Matches legacy MainWindow.zoomOutForBoundingBox distance computation. */
public void zoomOutForBoundingBox(double diagonalLength) {
double longestSide = diagonalLength * 0.4;
distance = longestSide / Math.tan(FOVY_RAD / 2.0);
panX = 0;
panY = 0;
}
public void reset() {
azimuthDeg = INITIAL_AZIMUTH;
elevationDeg = INITIAL_ELEVATION;
distance = INITIAL_DISTANCE;
panX = 0;
panY = 0;
}
/** Copies the orbit/zoom/pan state from another camera (but not its aspect ratio). */
public void copyOrbitFrom(OrbitCamera other) {
this.azimuthDeg = other.azimuthDeg;
this.elevationDeg = other.elevationDeg;
this.distance = other.distance;
this.panX = other.panX;
this.panY = other.panY;
}
public double distance() { return distance; }
/** Current azimuth (rotation about Z) in degrees. */
public double azimuthDeg() { return azimuthDeg; }
/** Current elevation (rotation about X) in degrees. */
public double elevationDeg() { return elevationDeg; }
/** Combined projection * view * model-rotation, column-major, ready for a GL uniform. */
public float[] viewProjection() {
float[] proj = Mat4.perspective((float) FOVY_RAD, aspect, NEAR, FAR);
float[] view = Mat4.translation((float) -panX, (float) -panY, (float) -distance);
float[] rot = Mat4.multiply(
Mat4.rotationX((float) Math.toRadians(elevationDeg)),
Mat4.rotationZ((float) Math.toRadians(azimuthDeg)));
return Mat4.multiply(proj, Mat4.multiply(view, rot));
}
}
package de.hft.stuttgart.citydoctor2.gui.gl;
import java.util.ArrayList;
import java.util.List;
/** Maps picking ids (1-based) to the model objects they represent. */
public class PickRegistry {
private final List<Object> targets = new ArrayList<>();
/** Registers a target and returns its non-zero picking id. */
public int register(Object target) {
targets.add(target);
return targets.size(); // id = index + 1
}
/** Resolves a picking id to its target, or null for background / unknown id. */
public Object resolve(int id) {
if (id <= 0 || id > targets.size()) {
return null;
}
return targets.get(id - 1);
}
public int size() { return targets.size(); }
}
package de.hft.stuttgart.citydoctor2.gui.gl;
/** Encodes integer ids into RGB bytes for GPU color-picking. Id 0 is reserved for background. */
public final class PickingCodec {
private PickingCodec() {}
public static int red(int id) { return id & 0xFF; }
public static int green(int id) { return (id >> 8) & 0xFF; }
public static int blue(int id) { return (id >> 16) & 0xFF; }
public static int decode(int r, int g, int b) {
return (r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16);
}
}
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);
}
}
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