mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-15 01:34:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c578970a66 | ||
|
|
ef68af5850 | ||
|
|
b6a4497fb3 | ||
|
|
939e31461a | ||
|
|
02f0b0fca1 | ||
|
|
b7d439e6b1 | ||
|
|
c36647af34 | ||
|
|
f787801a0e | ||
|
|
f192793395 | ||
|
|
3876f7e693 | ||
|
|
94ee37bfed | ||
|
|
26c161f0e8 |
No files matched your search
@@ -101,3 +101,5 @@ Targets GL 3.3+ core profile
|
||||
* FFP vertex shader generation inspired by ffvertex_prog.c (Copyright 2007 VMware, Inc.)
|
||||
* FFP fragment shader generation inspired by ff_fragment_shader.c (Copyright 2007, 2009 VMware, Inc. / 2010-2011 Intel Corporation)
|
||||
* Fog parameter optimization inspired by st_nir_lower_fog.c (Copyright 2023 Google LLC) and prog_statevars.c (Copyright 1999-2007 Brian Paul)
|
||||
* RGSS Terrain Texture Filtering
|
||||
* Indiesoft for [defold-sharp-sprite](https://github.com/indiesoftby/defold-sharp-sprite), used as the reference implementation under the MIT license
|
||||
@@ -103,6 +103,11 @@ repositories {
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
val spirvTestRuntime by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// MC compile stubs
|
||||
compileOnly(sourceSets["stubs"].output)
|
||||
@@ -133,6 +138,7 @@ dependencies {
|
||||
testRuntimeOnly(libs.lwjgl3.spvc) { artifact { classifier = lwjglNatives } }
|
||||
testRuntimeOnly(libs.lwjgl3)
|
||||
testRuntimeOnly(libs.lwjgl3) { artifact { classifier = lwjglNatives } }
|
||||
spirvTestRuntime(libs.lwjgl3.opengl)
|
||||
// @Lwjgl3Aware annotation
|
||||
compileOnly(libs.lwjgl3ify) { artifact { classifier = "dev" }; isTransitive = false }
|
||||
compileOnly(libs.retrofuturabootstrap) { isTransitive = false }
|
||||
@@ -309,7 +315,7 @@ val spirvTest by tasks.registering(Test::class) {
|
||||
classpath = sourceSets["test"].runtimeClasspath.filter { file ->
|
||||
// Strip the LWJGL2 jars - they collide with LWJGL3's sealed org.lwjgl package.
|
||||
!file.name.startsWith("lwjgl-2.") && !file.name.startsWith("lwjgl_util-") && !file.name.startsWith("lwjgl-platform-")
|
||||
}
|
||||
}.plus(spirvTestRuntime)
|
||||
|
||||
filter {
|
||||
includeTestsMatching("com.gtnewhorizons.angelica.glsm.shader.SpirvShaderTranslator*Test")
|
||||
|
||||
@@ -54,6 +54,7 @@ public final class SystemProperties {
|
||||
public static final boolean FLYBY_EXIT_WHEN_DONE = Boolean.getBoolean("angelica.flyby.exitWhenDone");
|
||||
public static final long FLYBY_TIME_OF_DAY = Math.floorMod(Long.getLong("angelica.flyby.timeOfDay", 6000L), 24000L);
|
||||
public static final String FLYBY_COMMANDS = System.getProperty("angelica.flyby.commands", "");
|
||||
public static final String FLYBY_ORIGIN = System.getProperty("angelica.flyby.origin", "");
|
||||
public static final FlybyPacing FLYBY_PACING = parseEnum("angelica.flyby.pacing", FlybyPacing.UNCAPPED, FlybyPacing.UNCAPPED, FlybyPacing.class);
|
||||
|
||||
// Debug
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import org.lwjgl.opengl.OpenGLException;
|
||||
|
||||
final class GLErrorReporter {
|
||||
|
||||
private GLErrorReporter() {}
|
||||
|
||||
static void reportError(int error) {
|
||||
throw new OpenGLException(error);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,10 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.FfpExtendedAttribs;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
|
||||
@@ -426,11 +428,7 @@ public class GLStateManager {
|
||||
public static boolean wideLineEmulationActive = false;
|
||||
public static boolean lineStippleActive = false;
|
||||
|
||||
public static boolean instancedFfpDrawActive = false;
|
||||
|
||||
|
||||
|
||||
|
||||
public static Instancing ffpInstancing = Instancing.NONE;
|
||||
|
||||
private static final MethodHandle MAT4_STACK_CURR_DEPTH;
|
||||
|
||||
@@ -1428,8 +1426,11 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static void endForeignDraw() {
|
||||
if (foreignDrawDepth > 0 && --foreignDrawDepth == 0 && GLSMHooks.FOREIGN_DRAW_END.hasListeners()) {
|
||||
GLSMHooks.FOREIGN_DRAW_END.post(GLSMHooks.foreignDrawEndEvent);
|
||||
if (foreignDrawDepth > 0 && --foreignDrawDepth == 0) {
|
||||
GLSMHooks.resolvePendingProgram();
|
||||
if (GLSMHooks.FOREIGN_DRAW_END.hasListeners()) {
|
||||
GLSMHooks.FOREIGN_DRAW_END.post(GLSMHooks.foreignDrawEndEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3577,8 +3578,7 @@ public class GLStateManager {
|
||||
|
||||
public static void updateDisplay(boolean processMessages) throws LWJGLException {
|
||||
if (Thread.currentThread() != MainThread) {
|
||||
swapBuffers();
|
||||
if (processMessages) pumpDisplayMessages();
|
||||
RENDER_BACKEND.updateDisplayFromWorkerThread(processMessages);
|
||||
return;
|
||||
}
|
||||
if (processMessages) pumpDisplayMessages();
|
||||
@@ -4062,6 +4062,14 @@ public class GLStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setTextureMatrix(int unit, Matrix4fc m) {
|
||||
if (isCachingEnabled()) {
|
||||
final GLContextState glCtx = ctx();
|
||||
glCtx.textures.getTextureUnitMatrix(unit).set(m);
|
||||
glCtx.texMatrixGeneration++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setProjectionMatrix(Matrix4fc m) {
|
||||
final GLContextState glCtx = ctx();
|
||||
if (isCachingEnabled()) {
|
||||
@@ -5390,6 +5398,11 @@ public class GLStateManager {
|
||||
return RENDER_BACKEND.getError();
|
||||
}
|
||||
|
||||
public static void checkGLError() {
|
||||
final int error = glGetError();
|
||||
if (error != GL11.GL_NO_ERROR) GLErrorReporter.reportError(error);
|
||||
}
|
||||
|
||||
public static String glGetString(int pname) {
|
||||
if (!RENDER_BACKEND.hasContext()) return "no valid GL/render context";
|
||||
return RENDER_BACKEND.getString(pname);
|
||||
|
||||
@@ -637,6 +637,11 @@ public class RenderSystem {
|
||||
GLStateManager.glSamplerParameteri(sampler, pname, param);
|
||||
}
|
||||
|
||||
public static void samplerParameterf(int sampler, int pname, float param) {
|
||||
if (!supportsSamplerObjects || sampler == 0) return;
|
||||
GLStateManager.glSamplerParameterf(sampler, pname, param);
|
||||
}
|
||||
|
||||
public static void bindSamplerToUnit(int unit, int sampler) {
|
||||
if (!supportsSamplerObjects) return;
|
||||
GLStateManager.glBindSampler(unit, sampler);
|
||||
|
||||
@@ -134,6 +134,12 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
return caps != null && caps.GL_EXT_texture_filter_anisotropic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplayFromWorkerThread(boolean processMessages) {
|
||||
if (processMessages) pumpDisplayMessages();
|
||||
Display.update(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {GL11.glFlush();}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.backend;
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import org.lwjgl.LWJGLException;
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
@@ -251,6 +252,13 @@ public abstract class RenderBackend {
|
||||
public boolean handleSwapBuffers() { return false; }
|
||||
public void onRenderThreadReleased(Thread thread) {}
|
||||
|
||||
public void updateDisplayFromWorkerThread(boolean processMessages) throws LWJGLException {
|
||||
GLStateManager.swapBuffers();
|
||||
if (processMessages) {
|
||||
pumpDisplayMessages();
|
||||
}
|
||||
}
|
||||
|
||||
public String getTransferDebugInfo() { return null; }
|
||||
|
||||
public boolean bindVoxelizationRegion(int ssboBinding, long openPass, float x, float y, float z) { return false; }
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
public final class CubeInstancedAttribs {
|
||||
|
||||
public static final int LOC_LIGHTMAP_SCALE = InstancedAttribs.LOC_LIGHTMAP;
|
||||
public static final int LOC_CUBE_TEX = 15;
|
||||
|
||||
public static final int OFFSET_LIGHTMAP_SCALE = InstancedAttribs.HEAD_SIZE;
|
||||
public static final int OFFSET_CUBE_TEX = OFFSET_LIGHTMAP_SCALE + 16;
|
||||
public static final int STRIDE = OFFSET_CUBE_TEX + 16;
|
||||
|
||||
public static void pointInstanceAttribs(long base) {
|
||||
InstancedAttribs.pointHead(base, STRIDE);
|
||||
GLStateManager.glVertexAttribPointer(LOC_LIGHTMAP_SCALE, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_LIGHTMAP_SCALE);
|
||||
GLStateManager.glVertexAttribPointer(LOC_CUBE_TEX, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_CUBE_TEX);
|
||||
}
|
||||
|
||||
static void enableInstanceArrays() {
|
||||
InstancedAttribs.enableHeadArrays();
|
||||
InstancedAttribs.enableInstanced(LOC_CUBE_TEX);
|
||||
}
|
||||
|
||||
private CubeInstancedAttribs() {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
|
||||
public record CubeParams(float minX, float minY, float minZ, float spanX, float spanY, float spanZ, float u0, float v0, float uScaleX, float vScaleY, float uScaleZ, float vScaleZ) {
|
||||
|
||||
private static final float MIN_SPAN = 1.0e-4f;
|
||||
|
||||
public CubeParams {
|
||||
if (!(spanX > MIN_SPAN && spanY > MIN_SPAN && spanZ > MIN_SPAN)) throw new IllegalArgumentException("degenerate cube span");
|
||||
}
|
||||
|
||||
public static CubeParams of(float texWidth, float texHeight, boolean mirror, int texU, int texV, float x, float y, float z, int sizeX, int sizeY, int sizeZ, float inflate) {
|
||||
if (texWidth <= 0.0f || texHeight <= 0.0f) return null;
|
||||
if (sizeX < 0 || sizeY < 0 || sizeZ < 0) return null;
|
||||
if (mirror && sizeX == 0) return null;
|
||||
final float minX = x - inflate;
|
||||
final float minY = y - inflate;
|
||||
final float minZ = z - inflate;
|
||||
final float spanX = ((x + sizeX) + inflate) - minX;
|
||||
final float spanY = ((y + sizeY) + inflate) - minY;
|
||||
final float spanZ = ((z + sizeZ) + inflate) - minZ;
|
||||
if (spanX <= MIN_SPAN || spanY <= MIN_SPAN || spanZ <= MIN_SPAN) return null;
|
||||
return new CubeParams(minX, minY, minZ, spanX, spanY, spanZ, texU / texWidth, texV / texHeight, (mirror ? -sizeX : sizeX) / texWidth, sizeY / texHeight, sizeZ / texWidth, sizeZ / texHeight);
|
||||
}
|
||||
|
||||
public void writeRows(long ptr, float[] mv, int off, float scale) {
|
||||
final float tx = minX * scale;
|
||||
final float ty = minY * scale;
|
||||
final float tz = minZ * scale;
|
||||
final float sx = spanX * scale;
|
||||
final float sy = spanY * scale;
|
||||
final float sz = spanZ * scale;
|
||||
for (int r = 0; r < 3; r++) {
|
||||
final float c0 = mv[off + r];
|
||||
final float c1 = mv[off + 4 + r];
|
||||
final float c2 = mv[off + 8 + r];
|
||||
final long row = ptr + InstancedAttribs.OFFSET_ROWS + r * 16L;
|
||||
memPutFloat(row, c0 * sx);
|
||||
memPutFloat(row + 4, c1 * sy);
|
||||
memPutFloat(row + 8, c2 * sz);
|
||||
memPutFloat(row + 12, mv[off + 12 + r] + c0 * tx + c1 * ty + c2 * tz);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeTexture(long ptr) {
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_LIGHTMAP_SCALE + 8, uScaleZ);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_LIGHTMAP_SCALE + 12, vScaleZ);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_CUBE_TEX, u0);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_CUBE_TEX + 4, v0);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_CUBE_TEX + 8, uScaleX);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_CUBE_TEX + 12, vScaleY);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import org.lwjgl.opengl.GL13;
|
||||
* Packed-long fragment shader permutation key for FFP emulation.
|
||||
*
|
||||
* Layout:
|
||||
* long[0]: global bits (13) + unit 0 (47 bits at offset 13)
|
||||
* long[0]: global bits (14) + unit 0 (47 bits at offset 14)
|
||||
* long[1..3]: unit 1..3 (47 bits each, low-aligned)
|
||||
* Only max(1, nrEnabledUnits) longs are significant.
|
||||
*
|
||||
@@ -63,7 +63,7 @@ public final class FragmentKey {
|
||||
public static final int FOG_EXP = 2;
|
||||
public static final int FOG_EXP2 = 3;
|
||||
|
||||
private static final int GLOBAL_BITS = 13;
|
||||
private static final int GLOBAL_BITS = 14;
|
||||
private static final int BIT_FOG_MODE = 0; // 2 bits
|
||||
private static final int BIT_ALPHA_TEST = 2; // 1 bit
|
||||
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
|
||||
@@ -72,6 +72,7 @@ public final class FragmentKey {
|
||||
private static final int BIT_OVERLAY_ENABLED = 10; // 1 bit
|
||||
private static final int BIT_LINE_STIPPLE = 11; // 1 bit
|
||||
private static final int BIT_COLOR_SUM = 12; // 1 bit
|
||||
private static final int BIT_OVERLAY_INSTANCED = 13;
|
||||
|
||||
private static final int U_ENABLED = 0;
|
||||
private static final int U_MODE = 1; // 3 bits
|
||||
@@ -123,7 +124,9 @@ public final class FragmentKey {
|
||||
}
|
||||
|
||||
// Damage overlay
|
||||
if (GLStateManager.getOverlayA() != 0.0f) {
|
||||
if (GLStateManager.ffpInstancing.hasInstanceHead()) {
|
||||
global |= (1L << BIT_OVERLAY_INSTANCED);
|
||||
} else if (GLStateManager.getOverlayA() != 0.0f) {
|
||||
global |= (1L << BIT_OVERLAY_ENABLED);
|
||||
}
|
||||
|
||||
@@ -245,6 +248,7 @@ public final class FragmentKey {
|
||||
public boolean lineStipple() { return ((packed[0] >> BIT_LINE_STIPPLE) & 1) != 0; }
|
||||
public int nrEnabledUnits() { return (int) ((packed[0] >> BIT_NR_ENABLED_UNITS) & 0x7); }
|
||||
public boolean overlayEnabled() { return ((packed[0] >> BIT_OVERLAY_ENABLED) & 1) != 0; }
|
||||
public boolean overlayInstanced() { return ((packed[0] >> BIT_OVERLAY_INSTANCED) & 1) != 0; }
|
||||
|
||||
private long unitBits(int i) {
|
||||
return (i == 0) ? (packed[0] >>> GLOBAL_BITS) : packed[i];
|
||||
@@ -378,10 +382,7 @@ public final class FragmentKey {
|
||||
default -> "?";
|
||||
};
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b colorSum=%b overlay=%b units=%d",
|
||||
fogName, alphaTestEnabled(),
|
||||
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-",
|
||||
separateSpecular(), colorSum(), overlayEnabled(), nrEnabledUnits()));
|
||||
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b colorSum=%b overlay=%b units=%d", fogName, alphaTestEnabled(), alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-", separateSpecular(), colorSum(), overlayEnabled() || overlayInstanced(), nrEnabledUnits()));
|
||||
for (int i = 0; i < nrEnabledUnits(); i++) {
|
||||
if (!unitEnabled(i)) {
|
||||
sb.append(String.format(" u%d=OFF", i));
|
||||
|
||||
@@ -35,6 +35,9 @@ public final class FragmentShaderGenerator {
|
||||
private static void emitInputs(StringBuilder sb, FragmentKey key) {
|
||||
sb.append("// Inputs from vertex shader\n");
|
||||
sb.append("in vec4 v_Color;\n");
|
||||
if (key.overlayInstanced()) {
|
||||
sb.append("in vec4 v_Overlay;\n");
|
||||
}
|
||||
if (key.separateSpecular()) {
|
||||
sb.append("in vec3 v_SpecularColor;\n");
|
||||
}
|
||||
@@ -116,8 +119,11 @@ public final class FragmentShaderGenerator {
|
||||
* Emit the modern-style damage overlay mix.
|
||||
*/
|
||||
private static void emitOverlay(StringBuilder sb, FragmentKey key) {
|
||||
if (!key.overlayEnabled()) return;
|
||||
sb.append(" color.rgb = mix(color.rgb, u_OverlayColor.rgb, u_OverlayColor.a);\n");
|
||||
if (key.overlayInstanced()) {
|
||||
sb.append(" color.rgb = mix(color.rgb, v_Overlay.rgb, v_Overlay.a);\n");
|
||||
} else if (key.overlayEnabled()) {
|
||||
sb.append(" color.rgb = mix(color.rgb, u_OverlayColor.rgb, u_OverlayColor.a);\n");
|
||||
}
|
||||
}
|
||||
|
||||
private static void emitSimpleUnit(StringBuilder sb, FragmentKey key, int unit, String texVar, String envColorVar, String prevVar, String assign) {
|
||||
|
||||
@@ -1,18 +1,84 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutLong;
|
||||
|
||||
public final class InstancedAttribs {
|
||||
|
||||
public static final int LOC_MATRIX_COL0 = 5;
|
||||
public static final int LOC_MATRIX_COL1 = 6;
|
||||
public static final int LOC_MATRIX_COL2 = 7;
|
||||
public static final int LOC_MATRIX_COL3 = 8;
|
||||
public static final int LOC_COLOR = 9;
|
||||
public static final int LOC_ROW0 = 5;
|
||||
public static final int LOC_ROW1 = 6;
|
||||
public static final int LOC_ROW2 = 7;
|
||||
public static final int LOC_COLOR = 8;
|
||||
public static final int LOC_OVERLAY = 9;
|
||||
public static final int LOC_LIGHTMAP = 10;
|
||||
public static final int LOC_ENTITY = 11;
|
||||
|
||||
public static final int OFFSET_MATRIX = 0;
|
||||
public static final int OFFSET_COLOR = 64;
|
||||
public static final int OFFSET_LIGHTMAP = 68;
|
||||
public static final int STRIDE = 80;
|
||||
public static final int OFFSET_ROWS = 0;
|
||||
public static final int OFFSET_COLOR = 48;
|
||||
public static final int OFFSET_OVERLAY = 52;
|
||||
public static final int OFFSET_ENTITY = 56;
|
||||
public static final int HEAD_SIZE = 64;
|
||||
public static final int OFFSET_LIGHTMAP = HEAD_SIZE;
|
||||
public static final int STRIDE = 72;
|
||||
|
||||
public static void writeHead(long ptr, float[] colMajor, int off, int colorABGR, int overlayABGR, long entityInfo) {
|
||||
writeRows(ptr, colMajor, off);
|
||||
writeTail(ptr, colorABGR, overlayABGR, entityInfo);
|
||||
}
|
||||
|
||||
private static void writeRows(long ptr, float[] colMajor, int off) {
|
||||
for (int r = 0; r < 3; r++) {
|
||||
final long row = ptr + OFFSET_ROWS + r * 16L;
|
||||
memPutFloat(row, colMajor[off + r]);
|
||||
memPutFloat(row + 4, colMajor[off + 4 + r]);
|
||||
memPutFloat(row + 8, colMajor[off + 8 + r]);
|
||||
memPutFloat(row + 12, colMajor[off + 12 + r]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeTail(long ptr, int colorABGR, int overlayABGR, long entityInfo) {
|
||||
memPutInt(ptr + OFFSET_COLOR, colorABGR);
|
||||
memPutInt(ptr + OFFSET_OVERLAY, overlayABGR);
|
||||
memPutLong(ptr + OFFSET_ENTITY, entityInfo);
|
||||
}
|
||||
|
||||
public static void writeLightmap(long ptr, int packedLight) {
|
||||
memPutFloat(ptr, packedLight & 0xFFFF);
|
||||
memPutFloat(ptr + 4, (packedLight >>> 16) & 0xFFFF);
|
||||
}
|
||||
|
||||
public static long packEntityInfo(int entity, int blockEntity, int item) {
|
||||
return (entity & 0xFFFFL) | ((blockEntity & 0xFFFFL) << 16) | ((item & 0xFFFFL) << 32);
|
||||
}
|
||||
|
||||
public static void pointHead(long base, int stride) {
|
||||
for (int r = 0; r < 3; r++) {
|
||||
GLStateManager.glVertexAttribPointer(LOC_ROW0 + r, 4, GL11.GL_FLOAT, false, stride, base + OFFSET_ROWS + r * 16L);
|
||||
}
|
||||
GLStateManager.glVertexAttribPointer(LOC_COLOR, 4, GL11.GL_UNSIGNED_BYTE, true, stride, base + OFFSET_COLOR);
|
||||
GLStateManager.glVertexAttribPointer(LOC_OVERLAY, 4, GL11.GL_UNSIGNED_BYTE, true, stride, base + OFFSET_OVERLAY);
|
||||
GLStateManager.glVertexAttribIPointer(LOC_ENTITY, 4, GL11.GL_SHORT, stride, base + OFFSET_ENTITY);
|
||||
}
|
||||
|
||||
public static void pointTemplate(long base) {
|
||||
pointHead(base, STRIDE);
|
||||
GLStateManager.glVertexAttribPointer(LOC_LIGHTMAP, 2, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_LIGHTMAP);
|
||||
}
|
||||
|
||||
public static void enableHeadArrays() {
|
||||
for (int loc = LOC_ROW0; loc <= LOC_ENTITY; loc++) {
|
||||
enableInstanced(loc);
|
||||
}
|
||||
}
|
||||
|
||||
static void enableInstanced(int loc) {
|
||||
GLStateManager.glEnableVertexAttribArray(loc);
|
||||
GLStateManager.glVertexAttribDivisor(loc, 1);
|
||||
}
|
||||
|
||||
private InstancedAttribs() {}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class InstancedGlslHelpers {
|
||||
|
||||
private InstancedGlslHelpers() {}
|
||||
|
||||
public static String mat4FromRows(String row0, String row1, String row2) {
|
||||
return "mat4("
|
||||
+ "vec4(" + row0 + ".x, " + row1 + ".x, " + row2 + ".x, 0.0), "
|
||||
+ "vec4(" + row0 + ".y, " + row1 + ".y, " + row2 + ".y, 0.0), "
|
||||
+ "vec4(" + row0 + ".z, " + row1 + ".z, " + row2 + ".z, 0.0), "
|
||||
+ "vec4(" + row0 + ".w, " + row1 + ".w, " + row2 + ".w, 1.0))";
|
||||
}
|
||||
|
||||
public static String mat3FromRows(String row0, String row1, String row2) {
|
||||
return "mat3("
|
||||
+ "normalize(vec3(" + row0 + ".x, " + row1 + ".x, " + row2 + ".x)), "
|
||||
+ "normalize(vec3(" + row0 + ".y, " + row1 + ".y, " + row2 + ".y)), "
|
||||
+ "normalize(vec3(" + row0 + ".z, " + row1 + ".z, " + row2 + ".z)))";
|
||||
}
|
||||
|
||||
public static String particlePos(String centerHalf, String offset) {
|
||||
return "vec4(" + centerHalf + ".xyz + " + offset + " * " + centerHalf + ".w, 1.0)";
|
||||
}
|
||||
|
||||
public static String particleUv(String uv, String corner) {
|
||||
return "vec4("
|
||||
+ "mix(" + uv + ".x, " + uv + ".z, " + corner + ".x), "
|
||||
+ "mix(" + uv + ".y, " + uv + ".w, " + corner + ".y), "
|
||||
+ "0.0, 1.0)";
|
||||
}
|
||||
|
||||
public static String cubePrelude(String prefix, String mid, String delta, String normal, String tex, String sep) {
|
||||
return "float " + prefix + "cubeMirror = " + tex + ".z < 0.0 ? 1.0 : 0.0;" + sep
|
||||
+ "vec2 " + prefix + "cubeMidU = " + mid + ".xy - " + prefix + "cubeMirror * " + normal + ".x;" + sep
|
||||
+ "vec2 " + prefix + "cubeU = " + prefix + "cubeMidU + (1.0 - 2.0 * " + prefix + "cubeMirror) * " + delta + ".xy;" + sep
|
||||
+ "vec2 " + prefix + "cubeV = " + mid + ".zw + " + delta + ".zw;";
|
||||
}
|
||||
|
||||
public static String cubeUv(String tex, String lightScale, String uA, String uZ, String vA, String vZ) {
|
||||
return "vec4("
|
||||
+ tex + ".x + " + uA + " * abs(" + tex + ".z) + " + uZ + " * " + lightScale + ".z, "
|
||||
+ tex + ".y + " + vA + " * " + tex + ".w + " + vZ + " * " + lightScale + ".w, "
|
||||
+ "0.0, 1.0)";
|
||||
}
|
||||
|
||||
public static String[] attributeDecls(String prefix, Instancing kind) {
|
||||
final ArrayList<String> decls = new ArrayList<>(9);
|
||||
switch (kind) {
|
||||
case NONE -> {}
|
||||
case TEMPLATE -> addHead(decls, prefix, "vec2", "InstLightmap");
|
||||
case CUBE -> {
|
||||
decls.add(decl(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation(), "vec4", prefix + "CubeMid"));
|
||||
decls.add(decl(VertexFormatElement.Usage.SECONDARY_UV.getAttributeLocation(), "vec4", prefix + "CubeDelta"));
|
||||
addHead(decls, prefix, "vec4", "InstLightmapScale");
|
||||
decls.add(decl(CubeInstancedAttribs.LOC_CUBE_TEX, "vec4", prefix + "CubeTex"));
|
||||
}
|
||||
case PARTICLE -> {
|
||||
decls.add(decl(ParticleInstancedAttribs.LOC_CENTER_HALF, "vec4", prefix + "InstCenterHalf"));
|
||||
decls.add(decl(ParticleInstancedAttribs.LOC_UV, "vec4", prefix + "InstUv"));
|
||||
decls.add(decl(ParticleInstancedAttribs.LOC_COLOR, "vec4", prefix + "InstColor"));
|
||||
decls.add(decl(ParticleInstancedAttribs.LOC_LIGHTMAP, "vec2", prefix + "InstLightmap"));
|
||||
}
|
||||
}
|
||||
return decls.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private static void addHead(ArrayList<String> decls, String prefix, String lightmapType, String lightmapName) {
|
||||
decls.add(decl(InstancedAttribs.LOC_ROW0, "vec4", prefix + "InstRow0"));
|
||||
decls.add(decl(InstancedAttribs.LOC_ROW1, "vec4", prefix + "InstRow1"));
|
||||
decls.add(decl(InstancedAttribs.LOC_ROW2, "vec4", prefix + "InstRow2"));
|
||||
decls.add(decl(InstancedAttribs.LOC_COLOR, "vec4", prefix + "InstColor"));
|
||||
decls.add(decl(InstancedAttribs.LOC_OVERLAY, "vec4", prefix + "InstOverlay"));
|
||||
decls.add(decl(InstancedAttribs.LOC_LIGHTMAP, lightmapType, prefix + lightmapName));
|
||||
}
|
||||
|
||||
private static String decl(int loc, String type, String name) {
|
||||
return "layout(location = " + loc + ") in " + type + " " + name + ";";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
public enum Instancing {
|
||||
NONE(""),
|
||||
TEMPLATE("_instanced"),
|
||||
CUBE("_cubeinstanced"),
|
||||
PARTICLE("_particleinstanced");
|
||||
|
||||
public static final Instancing[] VALUES = values();
|
||||
|
||||
public final String variantSuffix;
|
||||
|
||||
Instancing(String variantSuffix) {
|
||||
this.variantSuffix = variantSuffix;
|
||||
}
|
||||
|
||||
public boolean hasInstanceHead() {
|
||||
return this == TEMPLATE || this == CUBE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
||||
|
||||
public final class ParticleInstancedAttribs {
|
||||
|
||||
public static final int LOC_CENTER_HALF = 5;
|
||||
public static final int LOC_UV = 6;
|
||||
public static final int LOC_COLOR = InstancedAttribs.LOC_COLOR;
|
||||
public static final int LOC_LIGHTMAP = InstancedAttribs.LOC_LIGHTMAP;
|
||||
|
||||
public static final int OFFSET_CENTER_HALF = 0;
|
||||
public static final int OFFSET_UV = 16;
|
||||
public static final int OFFSET_COLOR = 32;
|
||||
public static final int OFFSET_LIGHTMAP = 36;
|
||||
public static final int STRIDE = 48;
|
||||
|
||||
public static void pointInstanceAttribs(long base) {
|
||||
GLStateManager.glVertexAttribPointer(LOC_CENTER_HALF, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_CENTER_HALF);
|
||||
GLStateManager.glVertexAttribPointer(LOC_UV, 4, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_UV);
|
||||
GLStateManager.glVertexAttribPointer(LOC_COLOR, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, base + OFFSET_COLOR);
|
||||
GLStateManager.glVertexAttribPointer(LOC_LIGHTMAP, 2, GL11.GL_FLOAT, false, STRIDE, base + OFFSET_LIGHTMAP);
|
||||
}
|
||||
|
||||
static void enableInstanceArrays() {
|
||||
InstancedAttribs.enableInstanced(LOC_CENTER_HALF);
|
||||
InstancedAttribs.enableInstanced(LOC_UV);
|
||||
InstancedAttribs.enableInstanced(LOC_COLOR);
|
||||
InstancedAttribs.enableInstanced(LOC_LIGHTMAP);
|
||||
}
|
||||
|
||||
public static void writeInstance(long ptr, float cx, float cy, float cz, float half, float u0, float v0, float u1, float v1, int colorABGR, int packedLight) {
|
||||
memPutFloat(ptr + OFFSET_CENTER_HALF, cx);
|
||||
memPutFloat(ptr + OFFSET_CENTER_HALF + 4, cy);
|
||||
memPutFloat(ptr + OFFSET_CENTER_HALF + 8, cz);
|
||||
memPutFloat(ptr + OFFSET_CENTER_HALF + 12, half);
|
||||
memPutFloat(ptr + OFFSET_UV, u0);
|
||||
memPutFloat(ptr + OFFSET_UV + 4, v0);
|
||||
memPutFloat(ptr + OFFSET_UV + 8, u1);
|
||||
memPutFloat(ptr + OFFSET_UV + 12, v1);
|
||||
memPutInt(ptr + OFFSET_COLOR, colorABGR);
|
||||
InstancedAttribs.writeLightmap(ptr + OFFSET_LIGHTMAP, packedLight);
|
||||
}
|
||||
|
||||
private ParticleInstancedAttribs() {}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
||||
|
||||
public final class ParticleQuadMesh {
|
||||
|
||||
public static final int VERTEX_FLAGS = VertexFlags.TEXTURE_BIT;
|
||||
|
||||
public static final int VERTEX_COUNT = 4;
|
||||
private static final int VERTEX_STRIDE = 24;
|
||||
|
||||
private static final int OFFSET_OFFSET = 0;
|
||||
private static final int OFFSET_CORNER = 12;
|
||||
private static final int OFFSET_TANGENT = 20;
|
||||
|
||||
private static final float[] CORNER_A = { -1.0f, -1.0f, 1.0f, 1.0f };
|
||||
private static final float[] CORNER_B = { -1.0f, 1.0f, 1.0f, -1.0f };
|
||||
|
||||
private static int vao;
|
||||
private static int vbo;
|
||||
private static ByteBuffer staging;
|
||||
private static boolean written;
|
||||
private static float lastRotX, lastRotXZ, lastRotZ, lastRotYZ, lastRotXY;
|
||||
|
||||
private ParticleQuadMesh() {}
|
||||
|
||||
public static float cornerA(int corner) {
|
||||
return CORNER_A[corner];
|
||||
}
|
||||
|
||||
public static float cornerB(int corner) {
|
||||
return CORNER_B[corner];
|
||||
}
|
||||
|
||||
public static float offsetX(float a, float b, float rotX, float rotYZ) {
|
||||
return a * rotX + b * rotYZ;
|
||||
}
|
||||
|
||||
public static float offsetY(float b, float rotXZ) {
|
||||
return b * rotXZ;
|
||||
}
|
||||
|
||||
public static float offsetZ(float a, float b, float rotZ, float rotXY) {
|
||||
return a * rotZ + b * rotXY;
|
||||
}
|
||||
|
||||
public static int vao() {
|
||||
if (vao == 0) build();
|
||||
return vao;
|
||||
}
|
||||
|
||||
public static void update(float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
if (vao == 0) build();
|
||||
if (written && rotX == lastRotX && rotXZ == lastRotXZ && rotZ == lastRotZ && rotYZ == lastRotYZ && rotXY == lastRotXY) {
|
||||
return;
|
||||
}
|
||||
lastRotX = rotX;
|
||||
lastRotXZ = rotXZ;
|
||||
lastRotZ = rotZ;
|
||||
lastRotYZ = rotYZ;
|
||||
lastRotXY = rotXY;
|
||||
written = true;
|
||||
|
||||
write(memAddress0(staging), rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
final int savedVBO = GLStateManager.getBoundVBO();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
staging.position(0).limit(VERTEX_COUNT * VERTEX_STRIDE);
|
||||
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0L, staging);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
|
||||
}
|
||||
|
||||
private static void write(long dst, float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
final int tangent = packTangent(rotX, 0.0f, rotZ);
|
||||
long ptr = dst;
|
||||
for (int i = 0; i < VERTEX_COUNT; i++) {
|
||||
final float a = cornerA(i);
|
||||
final float b = cornerB(i);
|
||||
memPutFloat(ptr + OFFSET_OFFSET, offsetX(a, b, rotX, rotYZ));
|
||||
memPutFloat(ptr + OFFSET_OFFSET + 4, offsetY(b, rotXZ));
|
||||
memPutFloat(ptr + OFFSET_OFFSET + 8, offsetZ(a, b, rotZ, rotXY));
|
||||
memPutFloat(ptr + OFFSET_CORNER, (a + 1.0f) * 0.5f);
|
||||
memPutFloat(ptr + OFFSET_CORNER + 4, (b + 1.0f) * 0.5f);
|
||||
memPutInt(ptr + OFFSET_TANGENT, tangent);
|
||||
ptr += VERTEX_STRIDE;
|
||||
}
|
||||
}
|
||||
|
||||
private static int packTangent(float x, float y, float z) {
|
||||
final float lenSq = x * x + y * y + z * z;
|
||||
final float nx, ny, nz;
|
||||
if (lenSq < 1.0e-12f) {
|
||||
nx = 1.0f;
|
||||
ny = 0.0f;
|
||||
nz = 0.0f;
|
||||
} else {
|
||||
final float inv = (float) (1.0 / Math.sqrt(lenSq));
|
||||
nx = x * inv;
|
||||
ny = y * inv;
|
||||
nz = z * inv;
|
||||
}
|
||||
return NormI8.pack(nx, ny, nz) | (127 << 24);
|
||||
}
|
||||
|
||||
private static void build() {
|
||||
final int savedVAO = GLStateManager.getBoundVAO();
|
||||
final int savedVBO = GLStateManager.getBoundVBO();
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, (long) VERTEX_COUNT * VERTEX_STRIDE, GL15.GL_DYNAMIC_DRAW);
|
||||
|
||||
attrib(VertexFormatElement.Usage.POSITION.getAttributeLocation(), 3, GL11.GL_FLOAT, false, OFFSET_OFFSET);
|
||||
attrib(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation(), 2, GL11.GL_FLOAT, false, OFFSET_CORNER);
|
||||
attrib(ImmediateExtendedAttribHandler.LOC_TANGENT, 4, GL11.GL_BYTE, true, OFFSET_TANGENT);
|
||||
|
||||
ParticleInstancedAttribs.enableInstanceArrays();
|
||||
|
||||
GLStateManager.glBindVertexArray(savedVAO);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
|
||||
|
||||
if (staging == null) staging = memAlloc(VERTEX_COUNT * VERTEX_STRIDE);
|
||||
written = false;
|
||||
}
|
||||
|
||||
private static void attrib(int loc, int size, int type, boolean normalized, int offset) {
|
||||
GLStateManager.glEnableVertexAttribArray(loc);
|
||||
GLStateManager.glVertexAttribPointer(loc, size, type, normalized, VERTEX_STRIDE, offset);
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
if (vbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
if (vao != 0) {
|
||||
GLStateManager.glDeleteVertexArrays(vao);
|
||||
vao = 0;
|
||||
}
|
||||
if (staging != null) {
|
||||
memFree(staging);
|
||||
staging = null;
|
||||
}
|
||||
written = false;
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,8 @@ public final class ShaderManager {
|
||||
final long vkPacked = VertexKey.packFromState(true, true, true, true, fragMask);
|
||||
final VertexKey vk = VertexKey.fromPacked(vkPacked);
|
||||
VertexShaderGenerator.generate(vk);
|
||||
VertexShaderGenerator.generate(VertexKey.fromPacked(VertexKey.withInstancing(vkPacked, Instancing.CUBE)));
|
||||
VertexShaderGenerator.generate(VertexKey.fromPacked(VertexKey.withInstancing(vkPacked, Instancing.PARTICLE)));
|
||||
FragmentShaderGenerator.generate(FragmentKey.fromPacked(fkScratch, fkLen));
|
||||
GeometryShaderGenerator.generate(vk);
|
||||
final Class<?>[] touched = { Program.class, ShaderCache.class, TessellatorStreamingDrawer.class, QuadConverter.class };
|
||||
@@ -127,6 +129,7 @@ public final class ShaderManager {
|
||||
}
|
||||
|
||||
public void preDraw() {
|
||||
GLSMHooks.resolvePendingProgram();
|
||||
final DeferredBlendHandler bh = GLSMHooks.blendHandler;
|
||||
if (bh != null) bh.flushDeferredBlend();
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
||||
|
||||
public final class UnitCubeMesh {
|
||||
|
||||
public static final int VERTEX_FLAGS = VertexFlags.COLOR_BIT | VertexFlags.TEXTURE_BIT | VertexFlags.NORMAL_BIT;
|
||||
|
||||
public static final int FACE_COUNT = 6;
|
||||
public static final int VERTEX_COUNT = 24;
|
||||
public static final int VERTEX_STRIDE = 64;
|
||||
|
||||
public static final int OFFSET_POSITION = 0;
|
||||
private static final int OFFSET_COLOR = 12;
|
||||
public static final int OFFSET_MID = 16;
|
||||
public static final int OFFSET_DELTA = 32;
|
||||
public static final int OFFSET_NORMAL = 48;
|
||||
public static final int OFFSET_TANGENT = 60;
|
||||
|
||||
public static final float[][] CORNERS = {
|
||||
{ 0, 0, 0 }, { 1, 0, 0 }, { 1, 1, 0 }, { 0, 1, 0 },
|
||||
{ 0, 0, 1 }, { 1, 0, 1 }, { 1, 1, 1 }, { 0, 1, 1 },
|
||||
};
|
||||
|
||||
public record Span(int a, int z) {
|
||||
public int texels(int sizeA, int sizeZ) {
|
||||
return a * sizeA + z * sizeZ;
|
||||
}
|
||||
}
|
||||
|
||||
public record Net(Span uLow, Span uHigh, Span vLow, Span vHigh) {
|
||||
public Span u(int vertex) {
|
||||
return vertex == 0 || vertex == 3 ? uHigh : uLow;
|
||||
}
|
||||
|
||||
public Span v(int vertex) {
|
||||
return vertex >= 2 ? vHigh : vLow;
|
||||
}
|
||||
}
|
||||
|
||||
public record Face(int[] corners, float[] normal, float[] tangent, Net net) {}
|
||||
|
||||
public static final Face[] FACES = {
|
||||
new Face(new int[] { 5, 1, 2, 6 }, new float[] { 1, 0, 0 }, new float[] { 0, 0, 1, 1 }, new Net(new Span(1, 1), new Span(1, 2), new Span(0, 1), new Span(1, 1))),
|
||||
new Face(new int[] { 0, 4, 7, 3 }, new float[] { -1, 0, 0 }, new float[] { 0, 0, -1, 1 }, new Net(new Span(0, 0), new Span(0, 1), new Span(0, 1), new Span(1, 1))),
|
||||
new Face(new int[] { 5, 4, 0, 1 }, new float[] { 0, -1, 0 }, new float[] { 1, 0, 0, 1 }, new Net(new Span(0, 1), new Span(1, 1), new Span(0, 0), new Span(0, 1))),
|
||||
new Face(new int[] { 2, 3, 7, 6 }, new float[] { 0, 1, 0 }, new float[] { 1, 0, 0, -1 }, new Net(new Span(1, 1), new Span(2, 1), new Span(0, 1), new Span(0, 0))),
|
||||
new Face(new int[] { 1, 0, 3, 2 }, new float[] { 0, 0, -1 }, new float[] { 1, 0, 0, 1 }, new Net(new Span(0, 1), new Span(1, 1), new Span(0, 1), new Span(1, 1))),
|
||||
new Face(new int[] { 4, 5, 6, 7 }, new float[] { 0, 0, 1 }, new float[] { -1, 0, 0, 1 }, new Net(new Span(1, 2), new Span(2, 2), new Span(0, 1), new Span(1, 1))),
|
||||
};
|
||||
|
||||
private static int vao;
|
||||
private static int vbo;
|
||||
|
||||
private UnitCubeMesh() {}
|
||||
|
||||
public static void write(long dst) {
|
||||
long ptr = dst;
|
||||
for (int f = 0; f < FACE_COUNT; f++) {
|
||||
final Face face = FACES[f];
|
||||
final Net net = face.net();
|
||||
final float midUA = (net.uLow().a() + net.uHigh().a()) * 0.5f;
|
||||
final float midUZ = (net.uLow().z() + net.uHigh().z()) * 0.5f;
|
||||
final float midVA = (net.vLow().a() + net.vHigh().a()) * 0.5f;
|
||||
final float midVZ = (net.vLow().z() + net.vHigh().z()) * 0.5f;
|
||||
for (int c = 0; c < 4; c++) {
|
||||
final float[] corner = CORNERS[face.corners()[c]];
|
||||
final Span u = net.u(c);
|
||||
final Span v = net.v(c);
|
||||
memPutFloat(ptr + OFFSET_POSITION, corner[0]);
|
||||
memPutFloat(ptr + OFFSET_POSITION + 4, corner[1]);
|
||||
memPutFloat(ptr + OFFSET_POSITION + 8, corner[2]);
|
||||
memPutInt(ptr + OFFSET_COLOR, 0xFFFFFFFF);
|
||||
memPutFloat(ptr + OFFSET_MID, midUA);
|
||||
memPutFloat(ptr + OFFSET_MID + 4, midUZ);
|
||||
memPutFloat(ptr + OFFSET_MID + 8, midVA);
|
||||
memPutFloat(ptr + OFFSET_MID + 12, midVZ);
|
||||
memPutFloat(ptr + OFFSET_DELTA, u.a() - midUA);
|
||||
memPutFloat(ptr + OFFSET_DELTA + 4, u.z() - midUZ);
|
||||
memPutFloat(ptr + OFFSET_DELTA + 8, v.a() - midVA);
|
||||
memPutFloat(ptr + OFFSET_DELTA + 12, v.z() - midVZ);
|
||||
memPutFloat(ptr + OFFSET_NORMAL, face.normal()[0]);
|
||||
memPutFloat(ptr + OFFSET_NORMAL + 4, face.normal()[1]);
|
||||
memPutFloat(ptr + OFFSET_NORMAL + 8, face.normal()[2]);
|
||||
final float[] tangent = face.tangent();
|
||||
memPutInt(ptr + OFFSET_TANGENT, packTangent(tangent[0], tangent[1], tangent[2], tangent[3]));
|
||||
ptr += VERTEX_STRIDE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int packTangent(float x, float y, float z, float w) {
|
||||
return NormI8.pack(x, y, z) | (((int) (w * 127.0f) & 0xFF) << 24);
|
||||
}
|
||||
|
||||
public static int vao() {
|
||||
if (vao == 0) build();
|
||||
return vao;
|
||||
}
|
||||
|
||||
private static void build() {
|
||||
final int savedVAO = GLStateManager.getBoundVAO();
|
||||
final int savedVBO = GLStateManager.getBoundVBO();
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
|
||||
final int bytes = VERTEX_COUNT * VERTEX_STRIDE;
|
||||
final ByteBuffer data = memAlloc(bytes);
|
||||
try {
|
||||
write(memAddress0(data));
|
||||
data.position(0).limit(bytes);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||
} finally {
|
||||
memFree(data);
|
||||
}
|
||||
|
||||
attrib(VertexFormatElement.Usage.POSITION.getAttributeLocation(), 3, GL11.GL_FLOAT, false, OFFSET_POSITION);
|
||||
attrib(VertexFormatElement.Usage.COLOR.getAttributeLocation(), 4, GL11.GL_UNSIGNED_BYTE, true, OFFSET_COLOR);
|
||||
attrib(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation(), 4, GL11.GL_FLOAT, false, OFFSET_MID);
|
||||
attrib(VertexFormatElement.Usage.SECONDARY_UV.getAttributeLocation(), 4, GL11.GL_FLOAT, false, OFFSET_DELTA);
|
||||
attrib(VertexFormatElement.Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_FLOAT, false, OFFSET_NORMAL);
|
||||
attrib(ImmediateExtendedAttribHandler.LOC_TANGENT, 4, GL11.GL_BYTE, true, OFFSET_TANGENT);
|
||||
|
||||
CubeInstancedAttribs.enableInstanceArrays();
|
||||
|
||||
GLStateManager.glBindVertexArray(savedVAO);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
|
||||
}
|
||||
|
||||
private static void attrib(int loc, int size, int type, boolean normalized, int offset) {
|
||||
GLStateManager.glEnableVertexAttribArray(loc);
|
||||
GLStateManager.glVertexAttribPointer(loc, size, type, normalized, VERTEX_STRIDE, offset);
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
if (vbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
if (vao != 0) {
|
||||
GLStateManager.glDeleteVertexArrays(vao);
|
||||
vao = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public final class VertexKey {
|
||||
private static final int BIT_UNIT_TEXMAT_BASE = BIT_WIDE_LINE + 1;
|
||||
private static final int BIT_UNIT23_UV_FROM_UNIT0 = BIT_UNIT_TEXMAT_BASE + MAX_UNITS;
|
||||
private static final int BIT_LINE_STIPPLE = BIT_UNIT23_UV_FROM_UNIT0 + 1;
|
||||
private static final int BIT_INSTANCED = BIT_LINE_STIPPLE + 1;
|
||||
private static final int BIT_INSTANCING = BIT_LINE_STIPPLE + 1;
|
||||
|
||||
public static final int TG_NONE = 0;
|
||||
public static final int TG_OBJ_LINEAR = 1;
|
||||
@@ -97,7 +97,7 @@ public final class VertexKey {
|
||||
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
|
||||
public boolean wideLineEmulation() { return bit(BIT_WIDE_LINE); }
|
||||
public boolean lineStipple() { return bit(BIT_LINE_STIPPLE); }
|
||||
public boolean instancedDraw() { return bit(BIT_INSTANCED); }
|
||||
public Instancing instancing() { return Instancing.VALUES[(int) ((packed >> BIT_INSTANCING) & 0x3)]; }
|
||||
|
||||
public boolean cmReplacesAmbient() { final int m = colorMaterialMode(); return m == CM_AMBIENT || m == CM_AMBIENT_AND_DIFFUSE; }
|
||||
public boolean cmReplacesDiffuse() { final int m = colorMaterialMode(); return m == CM_DIFFUSE || m == CM_AMBIENT_AND_DIFFUSE; }
|
||||
@@ -226,13 +226,15 @@ public final class VertexKey {
|
||||
bits |= (1L << BIT_LINE_STIPPLE);
|
||||
}
|
||||
|
||||
if (GLStateManager.instancedFfpDrawActive) {
|
||||
bits |= (1L << BIT_INSTANCED);
|
||||
}
|
||||
bits |= (long) GLStateManager.ffpInstancing.ordinal() << BIT_INSTANCING;
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
static long withInstancing(long packed, Instancing kind) {
|
||||
return (packed & ~(0x3L << BIT_INSTANCING)) | ((long) kind.ordinal() << BIT_INSTANCING);
|
||||
}
|
||||
|
||||
public static VertexKey fromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap, int fragUnitMask) {
|
||||
return new VertexKey(packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap, fragUnitMask));
|
||||
}
|
||||
@@ -255,12 +257,6 @@ public final class VertexKey {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("FFPVertexKey[0x%011X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b inst=%b]",
|
||||
packed, lightingEnabled(), lightEnabled(0), lightEnabled(1),
|
||||
colorMaterialEnabled(), fogEnabled(),
|
||||
unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0,
|
||||
unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0,
|
||||
hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(),
|
||||
texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), wideLineEmulation(), instancedDraw());
|
||||
return String.format("FFPVertexKey[0x%011X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), wideLineEmulation(), instancing());
|
||||
}
|
||||
}
|
||||
@@ -55,31 +55,43 @@ public final class VertexShaderGenerator {
|
||||
}
|
||||
|
||||
private static void emitAttributes(StringBuilder sb, VertexKey key) {
|
||||
final Instancing kind = key.instancing();
|
||||
sb.append("// Vertex attributes\n");
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.POSITION.getAttributeLocation()).append(") in vec3 a_Position;\n");
|
||||
if (key.hasVertexColor()) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.COLOR.getAttributeLocation()).append(") in vec4 a_Color;\n");
|
||||
}
|
||||
if (key.hasVertexTexCoord()) {
|
||||
if (kind == Instancing.PARTICLE) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord0;\n");
|
||||
} else {
|
||||
if (key.hasVertexColor()) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.COLOR.getAttributeLocation()).append(") in vec4 a_Color;\n");
|
||||
}
|
||||
if (kind != Instancing.CUBE) {
|
||||
if (key.hasVertexTexCoord()) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord0;\n");
|
||||
}
|
||||
if (key.hasVertexLightmap()) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.SECONDARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord1;\n");
|
||||
}
|
||||
}
|
||||
if (key.hasVertexNormal() || kind == Instancing.CUBE) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.NORMAL.getAttributeLocation()).append(") in vec3 a_Normal;\n");
|
||||
}
|
||||
}
|
||||
if (key.hasVertexLightmap()) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.SECONDARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord1;\n");
|
||||
}
|
||||
if (key.hasVertexNormal()) {
|
||||
sb.append("layout(location = ").append(VertexFormatElement.Usage.NORMAL.getAttributeLocation()).append(") in vec3 a_Normal;\n");
|
||||
}
|
||||
if (key.instancedDraw()) {
|
||||
sb.append("layout(location = ").append(InstancedAttribs.LOC_MATRIX_COL0).append(") in vec4 a_InstCol0;\n");
|
||||
sb.append("layout(location = ").append(InstancedAttribs.LOC_MATRIX_COL1).append(") in vec4 a_InstCol1;\n");
|
||||
sb.append("layout(location = ").append(InstancedAttribs.LOC_MATRIX_COL2).append(") in vec4 a_InstCol2;\n");
|
||||
sb.append("layout(location = ").append(InstancedAttribs.LOC_MATRIX_COL3).append(") in vec4 a_InstCol3;\n");
|
||||
sb.append("layout(location = ").append(InstancedAttribs.LOC_COLOR).append(") in vec4 a_InstColor;\n");
|
||||
sb.append("layout(location = ").append(InstancedAttribs.LOC_LIGHTMAP).append(") in vec2 a_InstLightmap;\n");
|
||||
for (String d : InstancedGlslHelpers.attributeDecls("a_", kind)) {
|
||||
sb.append(d).append('\n');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
private static final String PARTICLE_UV = InstancedGlslHelpers.particleUv("a_InstUv", "a_TexCoord0");
|
||||
|
||||
private static String unit0Source(VertexKey key) {
|
||||
return switch (key.instancing()) {
|
||||
case CUBE -> "cubeUv";
|
||||
case PARTICLE -> PARTICLE_UV;
|
||||
case NONE, TEMPLATE -> key.hasVertexTexCoord() ? "vec4(a_TexCoord0, 0.0, 1.0)" : "u_CurrentTexCoord0";
|
||||
};
|
||||
}
|
||||
|
||||
private static void emitLineStart(StringBuilder sb) {
|
||||
sb.append(" vec2 ndc = gl_Position.xy / gl_Position.w;\n");
|
||||
sb.append(" v_LineStart = u_Viewport.xy + (ndc * 0.5 + 0.5) * u_Viewport.zw;\n\n");
|
||||
@@ -93,6 +105,10 @@ public final class VertexShaderGenerator {
|
||||
private static void emitOutputs(StringBuilder sb, VertexKey key) {
|
||||
sb.append("// Outputs\n");
|
||||
sb.append("out vec4 v_Color;\n");
|
||||
if (key.instancing().hasInstanceHead()) {
|
||||
sb.append("out vec4 v_Overlay;\n");
|
||||
sb.append("invariant gl_Position;\n");
|
||||
}
|
||||
if (key.lineStipple()) {
|
||||
sb.append("flat out vec2 v_LineStart;\n");
|
||||
}
|
||||
@@ -116,11 +132,16 @@ public final class VertexShaderGenerator {
|
||||
|
||||
private static void emitPositionTransform(StringBuilder sb, VertexKey key) {
|
||||
sb.append(" // Position transform\n");
|
||||
sb.append(" vec4 pos4 = vec4(a_Position, 1.0);\n");
|
||||
if (key.instancedDraw()) {
|
||||
sb.append(" mat4 instMV = mat4(a_InstCol0, a_InstCol1, a_InstCol2, a_InstCol3);\n");
|
||||
if (key.instancing() == Instancing.PARTICLE) {
|
||||
sb.append(" vec4 pos4 = ").append(InstancedGlslHelpers.particlePos("a_InstCenterHalf", "a_Position")).append(";\n");
|
||||
} else {
|
||||
sb.append(" vec4 pos4 = vec4(a_Position, 1.0);\n");
|
||||
}
|
||||
if (key.instancing().hasInstanceHead()) {
|
||||
sb.append(" mat4 instMV = ").append(InstancedGlslHelpers.mat4FromRows("a_InstRow0", "a_InstRow1", "a_InstRow2")).append(";\n");
|
||||
sb.append(" vec4 eyePos = instMV * pos4;\n");
|
||||
sb.append(" gl_Position = u_ProjectionMatrix * eyePos;\n");
|
||||
sb.append(" v_Overlay = a_InstOverlay;\n");
|
||||
} else {
|
||||
sb.append(" gl_Position = u_MVPMatrix * pos4;\n");
|
||||
|
||||
@@ -134,8 +155,8 @@ public final class VertexShaderGenerator {
|
||||
|
||||
private static void emitNormalTransform(StringBuilder sb, VertexKey key) {
|
||||
sb.append(" // Normal transform\n");
|
||||
final String srcNormal = key.hasVertexNormal() ? "a_Normal" : "u_CurrentNormal";
|
||||
if (key.instancedDraw()) {
|
||||
final String srcNormal = key.hasVertexNormal() && key.instancing() != Instancing.PARTICLE ? "a_Normal" : "u_CurrentNormal";
|
||||
if (key.instancing().hasInstanceHead()) {
|
||||
sb.append(" vec3 normal = normalize(mat3(instMV) * ").append(srcNormal).append(");\n");
|
||||
sb.append('\n');
|
||||
return;
|
||||
@@ -156,9 +177,13 @@ public final class VertexShaderGenerator {
|
||||
if (key.colorMaterialEnabled()) {
|
||||
// Color material: vertex color replaces specific material properties per glColorMaterial mode (Mesa ffvertex_prog.c get_material() bitmask approach)
|
||||
sb.append(" vec4 matColor = ");
|
||||
sb.append(key.hasVertexColor() ? "a_Color" : "u_CurrentColor");
|
||||
if (key.instancedDraw()) {
|
||||
sb.append(" * a_InstColor");
|
||||
if (key.instancing() == Instancing.PARTICLE) {
|
||||
sb.append("a_InstColor");
|
||||
} else {
|
||||
sb.append(key.hasVertexColor() ? "a_Color" : "u_CurrentColor");
|
||||
if (key.instancing().hasInstanceHead()) {
|
||||
sb.append(" * a_InstColor");
|
||||
}
|
||||
}
|
||||
sb.append(";\n");
|
||||
|
||||
@@ -255,22 +280,27 @@ public final class VertexShaderGenerator {
|
||||
private static void emitColorPassthrough(StringBuilder sb, VertexKey key) {
|
||||
sb.append(" // Color passthrough (no lighting)\n");
|
||||
final String src = key.hasVertexColor() ? "a_Color" : "u_CurrentColor";
|
||||
if (key.instancedDraw()) {
|
||||
sb.append(" v_Color = ").append(src).append(" * a_InstColor;\n");
|
||||
} else {
|
||||
sb.append(" v_Color = ").append(src).append(";\n");
|
||||
switch (key.instancing()) {
|
||||
case PARTICLE -> sb.append(" v_Color = a_InstColor;\n");
|
||||
case TEMPLATE, CUBE -> sb.append(" v_Color = ").append(src).append(" * a_InstColor;\n");
|
||||
case NONE -> sb.append(" v_Color = ").append(src).append(";\n");
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
private static void emitTexCoordPassthrough(StringBuilder sb, VertexKey key) {
|
||||
final boolean cube = key.instancing() == Instancing.CUBE;
|
||||
final boolean unit0FromCube = cube && key.unitTexCoordEnabled(0) && !key.texGenEnabled();
|
||||
final boolean unit23FromCube = cube && key.unit23UvFromUnit0() && (key.unitTexCoordEnabled(2) || key.unitTexCoordEnabled(3));
|
||||
if (unit0FromCube || unit23FromCube) {
|
||||
emitCubeTexCoord(sb);
|
||||
}
|
||||
|
||||
if (key.texGenEnabled()) {
|
||||
emitTexGenCoordGeneration(sb, key);
|
||||
} else if (key.unitTexCoordEnabled(0)) {
|
||||
sb.append(" // Texture coordinates - unit 0\n");
|
||||
final String src = key.hasVertexTexCoord()
|
||||
? "vec4(a_TexCoord0, 0.0, 1.0)"
|
||||
: "u_CurrentTexCoord0";
|
||||
final String src = unit0Source(key);
|
||||
if (key.unitTexMatEnabled(0)) {
|
||||
sb.append(" v_TexCoord0 = u_TextureMatrix0 * ").append(src).append(";\n");
|
||||
} else {
|
||||
@@ -279,20 +309,17 @@ public final class VertexShaderGenerator {
|
||||
}
|
||||
|
||||
if (key.lightmapEnabled()) {
|
||||
if (key.instancedDraw()) {
|
||||
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(a_InstLightmap, 0.0, 1.0);\n");
|
||||
} else if (key.hasVertexLightmap()) {
|
||||
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(a_TexCoord1, 0.0, 1.0);\n");
|
||||
} else {
|
||||
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(u_CurrentLightmapCoord, 0.0, 1.0);\n");
|
||||
}
|
||||
final String lightmap = switch (key.instancing()) {
|
||||
case PARTICLE, TEMPLATE -> "a_InstLightmap";
|
||||
case CUBE -> "a_InstLightmapScale.xy";
|
||||
case NONE -> key.hasVertexLightmap() ? "a_TexCoord1" : "u_CurrentLightmapCoord";
|
||||
};
|
||||
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(").append(lightmap).append(", 0.0, 1.0);\n");
|
||||
}
|
||||
|
||||
for (int i = 2; i < VertexKey.MAX_UNITS; i++) {
|
||||
if (!key.unitTexCoordEnabled(i)) continue;
|
||||
final String src = key.unit23UvFromUnit0()
|
||||
? "vec4(a_TexCoord0, 0.0, 1.0)"
|
||||
: "u_CurrentTexCoord" + i;
|
||||
final String src = key.unit23UvFromUnit0() ? unit0Source(key) : "u_CurrentTexCoord" + i;
|
||||
if (!key.unit23UvFromUnit0() && key.unitTexMatEnabled(i)) {
|
||||
sb.append(" v_TexCoord").append(i).append(" = u_TextureMatrix").append(i).append(" * ").append(src).append(";\n");
|
||||
} else {
|
||||
@@ -301,6 +328,12 @@ public final class VertexShaderGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private static void emitCubeTexCoord(StringBuilder sb) {
|
||||
sb.append(" // Cube texture net\n");
|
||||
sb.append(" ").append(InstancedGlslHelpers.cubePrelude("", "a_CubeMid", "a_CubeDelta", "a_Normal", "a_CubeTex", "\n ")).append('\n');
|
||||
sb.append(" vec4 cubeUv = ").append(InstancedGlslHelpers.cubeUv("a_CubeTex", "a_InstLightmapScale", "cubeU.x", "cubeU.y", "cubeV.x", "cubeV.y")).append(";\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit per-coordinate texgen code following Mesa's ffvertex_prog.c build order.
|
||||
* OBJ_LINEAR: dot(pos4, objPlane), EYE_LINEAR: dot(eyePos, eyePlane).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm.hooks;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.*;
|
||||
import net.minecraftforge.eventbus.api.bus.EventBus;
|
||||
|
||||
@@ -14,6 +15,12 @@ public final class GLSMHooks {
|
||||
public static ShaderTransformPostProcessor postTransformProcessor;
|
||||
public static PerFrameUniformBlock perFrameUniformBlock;
|
||||
public static PerFrameUniformBlock perPassUniformBlock;
|
||||
public static PendingProgramSelection pendingProgramSelection;
|
||||
|
||||
public static void resolvePendingProgram() {
|
||||
final PendingProgramSelection p = pendingProgramSelection;
|
||||
if (p != null && !DisplayListManager.isRecording()) p.resolvePendingProgram();
|
||||
}
|
||||
|
||||
// EventBus instances
|
||||
public static final EventBus<TextureBindEvent> TEXTURE_BIND = EventBus.create(TextureBindEvent.class);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.gtnewhorizons.angelica.glsm.hooks;
|
||||
|
||||
public interface PendingProgramSelection {
|
||||
void resolvePendingProgram();
|
||||
}
|
||||
@@ -810,6 +810,8 @@ public class GLSMRedirector {
|
||||
.add("gluBuild2DMipmaps")
|
||||
.add("gluErrorString")
|
||||
);
|
||||
// Must honor disableErrorChecks like glGetError, or it throws on errors those checks left latched
|
||||
methodRedirects.put("org/lwjgl/opengl/Util", RedirectMap.newMap().add("checkGLError"));
|
||||
|
||||
// Quadric type replacements
|
||||
typeRedirects.put("org/lwjgl/util/glu/Sphere", "com/gtnewhorizons/angelica/glsm/compat/lwjgl/AngelicaSphere");
|
||||
|
||||
+46
@@ -1,8 +1,12 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.lwjgl.util.shaderc.Shaderc;
|
||||
@@ -12,6 +16,7 @@ import org.lwjgl.util.spvc.SpvcReflectedResource;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -33,6 +38,47 @@ class FFPUniformBlockSpirvLayoutTest {
|
||||
assertBlockLayout(FragmentShaderGenerator.generate(fk), Shaderc.shaderc_fragment_shader);
|
||||
}
|
||||
|
||||
static Stream<Arguments> instancedKeys() {
|
||||
return Stream.of(
|
||||
Arguments.of("cube-lightmap-texmat", cubeKey(true, true, false)),
|
||||
Arguments.of("cube-texgen", cubeKey(false, false, true)),
|
||||
Arguments.of("particle-fog-texmat", particleFogTexMatKey())
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@MethodSource("instancedKeys")
|
||||
void instancedVariantCompilesToSpirv(String label, VertexKey key) {
|
||||
final String glsl = VertexShaderGenerator.generate(key);
|
||||
final SpirvCompiler.Result result = SpirvCompiler.compile(glsl, Shaderc.shaderc_vertex_shader, label, SpirvCompiler.Options.vulkanForced460Core());
|
||||
assertNotNull(result.spirv(), () -> "SPIR-V compile failed: " + result.error() + "\n" + glsl);
|
||||
MemoryUtil.memFree(result.spirv());
|
||||
}
|
||||
|
||||
private static VertexKey cubeKey(boolean lightmapUnit, boolean unit0TextureMatrix, boolean texGen) {
|
||||
long bits = bit("BIT_HAS_VERTEX_COLOR") | bit("BIT_HAS_VERTEX_NORMAL") | bit("BIT_HAS_VERTEX_TEX") | bit("BIT_UNIT_TEX_BASE");
|
||||
if (lightmapUnit) bits |= 1L << (position("BIT_UNIT_TEX_BASE") + 1);
|
||||
if (unit0TextureMatrix || texGen) bits |= bit("BIT_UNIT_TEXMAT_BASE");
|
||||
if (texGen) {
|
||||
bits |= (long) VertexKey.TG_OBJ_LINEAR << position("BIT_TEXGEN_S");
|
||||
bits |= (long) VertexKey.TG_OBJ_LINEAR << position("BIT_TEXGEN_T");
|
||||
}
|
||||
return VertexKey.fromPacked(VertexKey.withInstancing(bits, Instancing.CUBE));
|
||||
}
|
||||
|
||||
private static VertexKey particleFogTexMatKey() {
|
||||
final long bits = bit("BIT_UNIT_TEX_BASE") | 1L << (position("BIT_UNIT_TEX_BASE") + 1) | bit("BIT_UNIT_TEXMAT_BASE") | bit("BIT_FOG");
|
||||
return VertexKey.fromPacked(VertexKey.withInstancing(bits, Instancing.PARTICLE));
|
||||
}
|
||||
|
||||
private static long bit(String name) {
|
||||
return 1L << position(name);
|
||||
}
|
||||
|
||||
private static int position(String name) {
|
||||
return Reflect.getStatic(VertexKey.class, name);
|
||||
}
|
||||
|
||||
private static void assertBlockLayout(String glsl, int shaderKind) {
|
||||
final SpirvCompiler.Result result = SpirvCompiler.compile(glsl, shaderKind, "ffp-layout-test", SpirvCompiler.Options.vulkanForced460Core());
|
||||
assertNotNull(result.spirv(), () -> "SPIR-V compile failed: " + result.error() + "\n" + glsl);
|
||||
|
||||
+251
-85
@@ -2,14 +2,23 @@ package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PendingProgramSelection;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.COLOR_BIT;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.NORMAL_BIT;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -22,41 +31,203 @@ class InstancedFFPDrawGLTest {
|
||||
private static final int TEMPLATE_STRIDE = 32;
|
||||
private static final int INSTANCE_STRIDE = InstancedAttribs.STRIDE;
|
||||
|
||||
private static final int PARITY_SIZE = 256;
|
||||
private static final int INTEGER_WIDTH = 300;
|
||||
private static final int INTEGER_HEIGHT = 100;
|
||||
|
||||
private static final List<String> log = new ArrayList<>();
|
||||
|
||||
private static final PendingProgramSelection LOGGING_SELECTION = () -> log.add("resolve");
|
||||
|
||||
private static final class LoggingBlendHandler implements DeferredBlendHandler {
|
||||
@Override
|
||||
public boolean isBlendLocked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOverrideHeld() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deferBlendModeToggle(boolean enabled) {}
|
||||
|
||||
@Override
|
||||
public void deferBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {}
|
||||
|
||||
@Override
|
||||
public void flushDeferredBlend() {
|
||||
log.add("flush");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean capturing;
|
||||
|
||||
private int vao, templateVbo, instanceVbo;
|
||||
|
||||
@BeforeAll
|
||||
static void registerForeignDrawEndListener() {
|
||||
capturing = true;
|
||||
GLSMHooks.FOREIGN_DRAW_END.addListener(event -> {
|
||||
if (capturing) log.add("listener");
|
||||
});
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void stopCapturing() {
|
||||
capturing = false;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
if (sm.isActive()) sm.deactivate();
|
||||
sm.disable();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLSMHooks.pendingProgramSelection = null;
|
||||
GLSMHooks.blendHandler = null;
|
||||
log.clear();
|
||||
FfpFixture.IntegerInstances.delete();
|
||||
FfpFixture.resetFfpState();
|
||||
CubeParityFixture.disableDirectionalLight();
|
||||
CubeParityFixture.deleteResources();
|
||||
ParticleParityFixture.deleteResources();
|
||||
if (templateVbo != 0) { GLStateManager.glDeleteBuffers(templateVbo); templateVbo = 0; }
|
||||
if (instanceVbo != 0) { GLStateManager.glDeleteBuffers(instanceVbo); instanceVbo = 0; }
|
||||
if (vao != 0) { GLStateManager.glDeleteVertexArrays(vao); vao = 0; }
|
||||
}
|
||||
|
||||
@Test
|
||||
void vertexKeyCarriesInstancedBit() {
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
final long plain = VertexKey.packFromState(true, true, false, false, 0);
|
||||
GLStateManager.instancedFfpDrawActive = true;
|
||||
final long instanced = VertexKey.packFromState(true, true, false, false, 0);
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
void drawArraysResolvesBeforeFlushDeferredBlend() {
|
||||
GLSMHooks.pendingProgramSelection = LOGGING_SELECTION;
|
||||
GLSMHooks.blendHandler = new LoggingBlendHandler();
|
||||
|
||||
assertFalse(VertexKey.fromPacked(plain).instancedDraw());
|
||||
assertTrue(VertexKey.fromPacked(instanced).instancedDraw());
|
||||
assertEquals(1, Long.bitCount(plain ^ instanced), "only the instanced bit may differ");
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 0);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
assertEquals(List.of("resolve", "flush"), log);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveIsSuppressedWhileDisplayListIsRecording() {
|
||||
GLSMHooks.pendingProgramSelection = LOGGING_SELECTION;
|
||||
final int list = GLStateManager.glGenLists(1);
|
||||
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLSMHooks.resolvePendingProgram();
|
||||
assertTrue(log.isEmpty());
|
||||
GLStateManager.glEndList();
|
||||
|
||||
GLSMHooks.resolvePendingProgram();
|
||||
assertEquals(List.of("resolve"), log);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nestedForeignDrawsResolveOnceAtOutermostEndBeforeListeners() {
|
||||
GLSMHooks.pendingProgramSelection = LOGGING_SELECTION;
|
||||
|
||||
GLStateManager.beginForeignDraw();
|
||||
GLStateManager.beginForeignDraw();
|
||||
GLStateManager.endForeignDraw();
|
||||
assertTrue(log.isEmpty());
|
||||
|
||||
GLStateManager.endForeignDraw();
|
||||
assertEquals(List.of("resolve", "listener"), log);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unitCubeMatchesBakedQuadsWithLightmap() {
|
||||
assertCubeParity(true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unitCubeMatchesBakedQuadsWhenLit() {
|
||||
assertCubeParity(false, true);
|
||||
}
|
||||
|
||||
private void assertCubeParity(boolean lightmap, boolean lit) {
|
||||
GLStateManager.glViewport(0, 0, PARITY_SIZE, PARITY_SIZE);
|
||||
CubeParityFixture.setupScene(lightmap, lit);
|
||||
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
sm.activate();
|
||||
|
||||
final CubeParityFixture.Spec[] specs = CubeParityFixture.specs();
|
||||
|
||||
FfpFixture.clear();
|
||||
CubeParityFixture.drawReference(specs, lightmap);
|
||||
final int[] reference = FfpFixture.readRegion(PARITY_SIZE);
|
||||
|
||||
FfpFixture.clear();
|
||||
CubeParityFixture.drawInstanced(specs);
|
||||
final int[] instanced = FfpFixture.readRegion(PARITY_SIZE);
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "parity draws must not raise a GL error");
|
||||
|
||||
CubeParityFixture.assertPixelParity(reference, instanced, PARITY_SIZE, 0xFF000000, pixel -> "0x" + Integer.toHexString(pixel));
|
||||
}
|
||||
|
||||
@Test
|
||||
void particleUnit0TextureMatrixMatchesReference() {
|
||||
final ParticleParityFixture.Rotation rot = ParticleParityFixture.Rotation.of(35f, -12f, false);
|
||||
GLStateManager.glViewport(0, 0, PARITY_SIZE, PARITY_SIZE);
|
||||
ParticleParityFixture.setupScene(true);
|
||||
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
sm.activate();
|
||||
|
||||
final ParticleParityFixture.Particle[] particles = ParticleParityFixture.particles();
|
||||
|
||||
FfpFixture.clear();
|
||||
ParticleParityFixture.drawReference(particles, rot.x(), rot.xz(), rot.z(), rot.yz(), rot.xy());
|
||||
final int[] reference = FfpFixture.readRegion(PARITY_SIZE);
|
||||
|
||||
FfpFixture.clear();
|
||||
ParticleParityFixture.drawInstanced(particles, rot.x(), rot.xz(), rot.z(), rot.yz(), rot.xy());
|
||||
final int[] instanced = FfpFixture.readRegion(PARITY_SIZE);
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "parity draws must not raise a GL error");
|
||||
ParticleParityFixture.assertPixelParity(reference, instanced, PARITY_SIZE, 0xFF000000, pixel -> "0x" + Integer.toHexString(pixel));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityAttribSelectsColorPerInstance() {
|
||||
GLStateManager.glViewport(0, 0, INTEGER_WIDTH, INTEGER_HEIGHT);
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.disableCull();
|
||||
|
||||
FfpFixture.IntegerInstances.build(0.66f);
|
||||
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
FfpFixture.IntegerInstances.draw();
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced ivec4 draw must not raise a GL error");
|
||||
|
||||
assertPixel(51, 50, 255, 0, 0, "entity -1 must render red");
|
||||
assertPixel(150, 50, 0, 255, 0, "entity 7 must render green");
|
||||
assertPixel(249, 50, 0, 0, 255, "entity 42 must render blue");
|
||||
}
|
||||
|
||||
private static void assertPixel(int x, int y, int r, int g, int b, String label) {
|
||||
final ByteBuffer pixel = BufferUtils.createByteBuffer(4);
|
||||
GL11.glReadPixels(x, y, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel);
|
||||
final int gotR = pixel.get(0) & 0xFF;
|
||||
final int gotG = pixel.get(1) & 0xFF;
|
||||
final int gotB = pixel.get(2) & 0xFF;
|
||||
assertEquals(r, gotR, () -> label + ": red channel, got (" + gotR + "," + gotG + "," + gotB + ")");
|
||||
assertEquals(g, gotG, () -> label + ": green channel, got (" + gotR + "," + gotG + "," + gotB + ")");
|
||||
assertEquals(b, gotB, () -> label + ": blue channel, got (" + gotR + "," + gotG + "," + gotB + ")");
|
||||
}
|
||||
|
||||
@Test
|
||||
void instancedSourceUsesInstanceAttribs() {
|
||||
GLStateManager.instancedFfpDrawActive = true;
|
||||
GLStateManager.ffpInstancing = Instancing.TEMPLATE;
|
||||
final VertexKey key = VertexKey.fromState(true, true, false, false, 0b10);
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
GLStateManager.ffpInstancing = Instancing.NONE;
|
||||
|
||||
final String source = VertexShaderGenerator.generate(key);
|
||||
assertTrue(source.contains("a_InstCol0"), "instance matrix attribs declared");
|
||||
assertTrue(source.contains("a_InstRow0"), "instance matrix attribs declared");
|
||||
assertTrue(source.contains("mat4 instMV"), "instance matrix used as modelview");
|
||||
assertTrue(source.contains("a_InstColor"), "instance color multiplier used");
|
||||
assertTrue(source.contains("a_InstLightmap"), "instance lightmap coord used");
|
||||
@@ -74,7 +245,7 @@ class InstancedFFPDrawGLTest {
|
||||
GLStateManager.disableCull();
|
||||
|
||||
buildTemplate();
|
||||
buildInstances();
|
||||
uploadInstances(0xFF0000FF, 0, 0xFF00FF00, 0);
|
||||
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
@@ -83,14 +254,12 @@ class InstancedFFPDrawGLTest {
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
GLStateManager.instancedFfpDrawActive = true;
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_TRIANGLES, 0, 3, 2);
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
drawInstances();
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced draw must not raise a GL error");
|
||||
|
||||
final int[] left = readPixel(200, 300);
|
||||
final int[] right = readPixel(600, 300);
|
||||
final int[] left = FfpFixture.readPixel(200, 300);
|
||||
final int[] right = FfpFixture.readPixel(600, 300);
|
||||
assertTrue(left[0] > 200 && left[1] < 50, "left instance red, got " + left[0] + "," + left[1] + "," + left[2]);
|
||||
assertTrue(right[1] > 200 && right[0] < 50, "right instance green, got " + right[0] + "," + right[1] + "," + right[2]);
|
||||
}
|
||||
@@ -106,7 +275,7 @@ class InstancedFFPDrawGLTest {
|
||||
GLStateManager.disableCull();
|
||||
|
||||
buildTemplate();
|
||||
buildInstancesWithAlpha();
|
||||
uploadInstances(0xFF0000FF, 0, 0x0D0000FF, 0);
|
||||
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
@@ -117,14 +286,12 @@ class InstancedFFPDrawGLTest {
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
GLStateManager.instancedFfpDrawActive = true;
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_TRIANGLES, 0, 3, 2);
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
drawInstances();
|
||||
GLStateManager.disableAlphaTest();
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
|
||||
final int[] left = readPixel(200, 300);
|
||||
final int[] right = readPixel(600, 300);
|
||||
final int[] left = FfpFixture.readPixel(200, 300);
|
||||
final int[] right = FfpFixture.readPixel(600, 300);
|
||||
assertTrue(left[0] > 200, "opaque instance drawn, got " + left[0]);
|
||||
assertTrue(right[0] < 20 && right[1] < 20, "alpha 0.05 instance discarded, got " + right[0] + "," + right[1]);
|
||||
}
|
||||
@@ -141,7 +308,7 @@ class InstancedFFPDrawGLTest {
|
||||
GLStateManager.disableCull();
|
||||
|
||||
buildTemplate();
|
||||
buildInstances();
|
||||
uploadInstances(0xFF0000FF, 0, 0xFF00FF00, 0);
|
||||
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
@@ -150,48 +317,71 @@ class InstancedFFPDrawGLTest {
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
GLStateManager.instancedFfpDrawActive = true;
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_TRIANGLES, 0, 3, 2);
|
||||
drawInstances();
|
||||
|
||||
GLStateManager.glDepthFunc(GL11.GL_EQUAL);
|
||||
GLStateManager.enableBlend();
|
||||
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GLStateManager.glColor4f(1f, 1f, 1f, 1f);
|
||||
rewriteInstanceColors(0x800000FF, 0x8000FF00);
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_TRIANGLES, 0, 3, 2);
|
||||
GLStateManager.instancedFfpDrawActive = false;
|
||||
uploadInstances(0x800000FF, 0, 0x8000FF00, 0);
|
||||
drawInstances();
|
||||
|
||||
GLStateManager.disableBlend();
|
||||
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
|
||||
GLStateManager.disableDepthTest();
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
|
||||
final int[] left = readPixel(200, 300);
|
||||
final int[] background = readPixel(400, 550);
|
||||
final int[] left = FfpFixture.readPixel(200, 300);
|
||||
final int[] background = FfpFixture.readPixel(400, 550);
|
||||
assertTrue(left[0] > 200, "overlay blended over base, got " + left[0]);
|
||||
assertTrue(background[0] < 20 && background[1] < 20, "overlay must not draw off-base, got " + background[0]);
|
||||
}
|
||||
|
||||
private void rewriteInstanceColors(int leftABGR, int rightABGR) {
|
||||
final ByteBuffer patch = BufferUtils.createByteBuffer(2 * INSTANCE_STRIDE);
|
||||
putInstance(patch, -0.5f, leftABGR);
|
||||
putInstance(patch, 0.5f, rightABGR);
|
||||
patch.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, patch, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
@Test
|
||||
void perInstanceOverlayMixesOnlyTintedInstances() {
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glViewport(0, 0, 800, 600);
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.disableCull();
|
||||
|
||||
buildTemplate();
|
||||
uploadInstances(0xFFFFFFFF, 0, 0xFFFFFFFF, 0xFF0000FF);
|
||||
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
sm.activate();
|
||||
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
GLStateManager.setOverlayColor(0f, 1f, 0f, 1f);
|
||||
drawInstances();
|
||||
GLStateManager.setOverlayColor(0f, 0f, 0f, 0f);
|
||||
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced overlay draw must not raise a GL error");
|
||||
|
||||
final int[] left = FfpFixture.readPixel(200, 300);
|
||||
final int[] right = FfpFixture.readPixel(600, 300);
|
||||
assertTrue(left[0] > 200 && left[1] > 200 && left[2] > 200, "left instance overlay alpha 0 keeps its base color, got " + left[0] + "," + left[1] + "," + left[2]);
|
||||
assertTrue(right[0] > 200 && right[1] < 50 && right[2] < 50, "right instance is mixed to its own opaque red overlay, got " + right[0] + "," + right[1] + "," + right[2]);
|
||||
}
|
||||
|
||||
private void buildInstancesWithAlpha() {
|
||||
private void uploadInstances(int leftColor, int leftOverlay, int rightColor, int rightOverlay) {
|
||||
final ByteBuffer instances = BufferUtils.createByteBuffer(2 * INSTANCE_STRIDE);
|
||||
putInstance(instances, -0.5f, 0xFF0000FF);
|
||||
putInstance(instances, 0.5f, 0x0D0000FF);
|
||||
putInstance(instances, -0.5f, leftColor, leftOverlay);
|
||||
putInstance(instances, 0.5f, rightColor, rightOverlay);
|
||||
instances.flip();
|
||||
|
||||
instanceVbo = GLStateManager.glGenBuffers();
|
||||
final boolean first = instanceVbo == 0;
|
||||
if (first) instanceVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, instances, GL15.GL_STATIC_DRAW);
|
||||
pointAndEnableInstanceAttribs();
|
||||
if (first) {
|
||||
pointAndEnableInstanceAttribs();
|
||||
}
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
@@ -225,47 +415,23 @@ class InstancedFFPDrawGLTest {
|
||||
buf.position(base + TEMPLATE_STRIDE);
|
||||
}
|
||||
|
||||
private void buildInstances() {
|
||||
final ByteBuffer instances = BufferUtils.createByteBuffer(2 * INSTANCE_STRIDE);
|
||||
putInstance(instances, -0.5f, 0xFF0000FF);
|
||||
putInstance(instances, 0.5f, 0xFF00FF00);
|
||||
instances.flip();
|
||||
|
||||
instanceVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, instances, GL15.GL_STATIC_DRAW);
|
||||
pointAndEnableInstanceAttribs();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
private static void pointAndEnableInstanceAttribs() {
|
||||
for (int c = 0; c < 4; c++) {
|
||||
final int loc = InstancedAttribs.LOC_MATRIX_COL0 + c;
|
||||
GLStateManager.glEnableVertexAttribArray(loc);
|
||||
GLStateManager.glVertexAttribPointer(loc, 4, GL11.GL_FLOAT, false, INSTANCE_STRIDE, c * 16L);
|
||||
GLStateManager.glVertexAttribDivisor(loc, 1);
|
||||
}
|
||||
GLStateManager.glEnableVertexAttribArray(InstancedAttribs.LOC_COLOR);
|
||||
GLStateManager.glVertexAttribPointer(InstancedAttribs.LOC_COLOR, 4, GL11.GL_UNSIGNED_BYTE, true, INSTANCE_STRIDE, InstancedAttribs.OFFSET_COLOR);
|
||||
GLStateManager.glVertexAttribDivisor(InstancedAttribs.LOC_COLOR, 1);
|
||||
GLStateManager.glEnableVertexAttribArray(InstancedAttribs.LOC_LIGHTMAP);
|
||||
GLStateManager.glVertexAttribPointer(InstancedAttribs.LOC_LIGHTMAP, 2, GL11.GL_FLOAT, false, INSTANCE_STRIDE, InstancedAttribs.OFFSET_LIGHTMAP);
|
||||
GLStateManager.glVertexAttribDivisor(InstancedAttribs.LOC_LIGHTMAP, 1);
|
||||
InstancedAttribs.enableHeadArrays();
|
||||
InstancedAttribs.pointTemplate(0L);
|
||||
}
|
||||
|
||||
private void putInstance(ByteBuffer buf, float translateX, int colorABGR) {
|
||||
private static void putInstance(ByteBuffer buf, float translateX, int colorABGR, int overlayABGR) {
|
||||
final int base = buf.position();
|
||||
buf.putFloat(base, 1f).putFloat(base + 20, 1f).putFloat(base + 40, 1f).putFloat(base + 60, 1f);
|
||||
buf.putFloat(base + 48, translateX);
|
||||
buf.putInt(base + InstancedAttribs.OFFSET_COLOR, colorABGR);
|
||||
buf.putFloat(base + InstancedAttribs.OFFSET_LIGHTMAP, 240f);
|
||||
buf.putFloat(base + InstancedAttribs.OFFSET_LIGHTMAP + 4, 240f);
|
||||
final long ptr = memAddress0(buf) + base;
|
||||
InstancedAttribs.writeHead(ptr, FfpFixture.translationMatrix(translateX), 0, colorABGR, overlayABGR, 0L);
|
||||
memPutFloat(ptr + InstancedAttribs.OFFSET_LIGHTMAP, 240f);
|
||||
memPutFloat(ptr + InstancedAttribs.OFFSET_LIGHTMAP + 4, 240f);
|
||||
buf.position(base + INSTANCE_STRIDE);
|
||||
}
|
||||
|
||||
private static int[] readPixel(int x, int y) {
|
||||
final ByteBuffer pixel = BufferUtils.createByteBuffer(4);
|
||||
GL11.glReadPixels(x, y, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel);
|
||||
return new int[] { pixel.get(0) & 0xFF, pixel.get(1) & 0xFF, pixel.get(2) & 0xFF };
|
||||
private static void drawInstances() {
|
||||
GLStateManager.ffpInstancing = Instancing.TEMPLATE;
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_TRIANGLES, 0, 3, 2);
|
||||
GLStateManager.ffpInstancing = Instancing.NONE;
|
||||
}
|
||||
}
|
||||
@@ -276,6 +276,11 @@ class GLSMRedirectorTest {
|
||||
assertTrue(unmappedSeen().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void utilCheckGLErrorIsRedirected() throws Exception {
|
||||
assertRedirectedToDeclaredTarget(new String[][]{{"org/lwjgl/opengl/Util", "checkGLError", "()V"}});
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexedGettersHaveRealTargets() throws Exception {
|
||||
final Class<?> glsm = Class.forName(GLSM.replace('/', '.'), false, getClass().getClassLoader());
|
||||
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
|
||||
public final class CubeParityFixture {
|
||||
|
||||
private static final int TEX_WIDTH = 64;
|
||||
private static final int TEX_HEIGHT = 32;
|
||||
private static final float SCALE = 0.0625f;
|
||||
private static final short BRIGHTNESS = 240;
|
||||
|
||||
public record Spec(int texU, int texV, float x, float y, float z, int sizeX, int sizeY, int sizeZ, float inflate, boolean mirror) {}
|
||||
|
||||
private static final float[] MODELVIEW = {
|
||||
1.25f, 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 1.25f, 0.0f, 0.0f,
|
||||
0.375f, 0.25f, -0.25f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f,
|
||||
};
|
||||
|
||||
private static final float[] MODELVIEW_CONFORMAL = {
|
||||
1.0f, 0.0f, 0.75f, 0.0f,
|
||||
0.0f, 1.25f, 0.0f, 0.0f,
|
||||
-0.75f, 0.0f, 1.0f, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, 1.0f,
|
||||
};
|
||||
|
||||
private static float[] modelView = MODELVIEW;
|
||||
|
||||
private static int netTexture;
|
||||
private static int lightmapTexture;
|
||||
|
||||
private CubeParityFixture() {}
|
||||
|
||||
public static Spec[] specs() {
|
||||
return new Spec[] {
|
||||
new Spec(0, 0, -4, -6, -2, 8, 12, 4, 0.0f, false),
|
||||
new Spec(16, 8, -3, 1, -3, 6, 4, 6, 0.25f, true),
|
||||
};
|
||||
}
|
||||
|
||||
private static void setupCamera(boolean conformal) {
|
||||
modelView = conformal ? MODELVIEW_CONFORMAL : MODELVIEW;
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
final FloatBuffer mv = BufferUtils.createFloatBuffer(16);
|
||||
mv.put(modelView).flip();
|
||||
GLStateManager.glLoadMatrix(mv);
|
||||
}
|
||||
|
||||
public static void setupScene(boolean lightmap, boolean lit) {
|
||||
GLStateManager.glDisable(GL11.GL_FOG);
|
||||
GLStateManager.glDisable(GL11.GL_BLEND);
|
||||
GLStateManager.glDisable(GL11.GL_ALPHA_TEST);
|
||||
GLStateManager.glEnable(GL11.GL_DEPTH_TEST);
|
||||
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
|
||||
GLStateManager.glDepthMask(true);
|
||||
GLStateManager.glEnable(GL11.GL_CULL_FACE);
|
||||
GLStateManager.glCullFace(GL11.GL_BACK);
|
||||
setupCamera(lit);
|
||||
if (lit) {
|
||||
enableDirectionalLight();
|
||||
} else {
|
||||
GLStateManager.glDisable(GL11.GL_LIGHTING);
|
||||
}
|
||||
|
||||
netTexture = createNetTexture();
|
||||
if (lightmap) {
|
||||
lightmapTexture = createNetTexture();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, lightmapTexture);
|
||||
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glScalef(0.00390625f, 0.00390625f, 0.00390625f);
|
||||
GLStateManager.glTranslatef(8.0f, 8.0f, 8.0f);
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, netTexture);
|
||||
}
|
||||
}
|
||||
|
||||
private static void enableDirectionalLight() {
|
||||
final FloatBuffer buf = BufferUtils.createFloatBuffer(4);
|
||||
GLStateManager.glEnable(GL11.GL_LIGHTING);
|
||||
GLStateManager.glEnable(GL11.GL_NORMALIZE);
|
||||
GLStateManager.glEnable(GL11.GL_LIGHT0);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHT1);
|
||||
GLStateManager.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, fill(buf, 0.2f, 0.2f, 0.2f, 1.0f));
|
||||
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_POSITION, fill(buf, 0.375f, 0.75f, 0.5f, 0.0f));
|
||||
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, fill(buf, 1.0f, 1.0f, 1.0f, 1.0f));
|
||||
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_AMBIENT, fill(buf, 0.0f, 0.0f, 0.0f, 1.0f));
|
||||
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_SPECULAR, fill(buf, 0.0f, 0.0f, 0.0f, 1.0f));
|
||||
GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL);
|
||||
GLStateManager.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_AMBIENT_AND_DIFFUSE);
|
||||
}
|
||||
|
||||
public static void disableDirectionalLight() {
|
||||
GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHT0);
|
||||
GLStateManager.glDisable(GL11.GL_NORMALIZE);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHTING);
|
||||
}
|
||||
|
||||
private static FloatBuffer fill(FloatBuffer buf, float x, float y, float z, float w) {
|
||||
buf.clear();
|
||||
buf.put(x).put(y).put(z).put(w).flip();
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static int createNetTexture() {
|
||||
final int id = GLStateManager.glGenTextures();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
|
||||
final ByteBuffer texels = BufferUtils.createByteBuffer(TEX_WIDTH * TEX_HEIGHT * 4);
|
||||
for (int y = 0; y < TEX_HEIGHT; y++) {
|
||||
for (int x = 0; x < TEX_WIDTH; x++) {
|
||||
texels.put((byte) (16 + x * 3));
|
||||
texels.put((byte) (16 + y * 7));
|
||||
texels.put((byte) (255 - x * 3));
|
||||
texels.put((byte) 255);
|
||||
}
|
||||
}
|
||||
texels.flip();
|
||||
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, TEX_WIDTH, TEX_HEIGHT, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
|
||||
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
|
||||
return id;
|
||||
}
|
||||
|
||||
private static final int REF_STRIDE = 44;
|
||||
|
||||
private static int referenceVao;
|
||||
private static int referenceVbo;
|
||||
|
||||
private static ByteBuffer buildQuadData(Spec[] specs) {
|
||||
final ByteBuffer data = BufferUtils.createByteBuffer(specs.length * 24 * REF_STRIDE);
|
||||
for (Spec spec : specs) {
|
||||
final float[] corners = corners(spec);
|
||||
for (int face = 0; face < UnitCubeMesh.FACE_COUNT; face++) {
|
||||
final float[] uv = faceRect(spec, face);
|
||||
final float[] normal = UnitCubeMesh.FACES[face].normal();
|
||||
for (int corner = 0; corner < 4; corner++) {
|
||||
final int source = spec.mirror() ? 3 - corner : corner;
|
||||
final int index = UnitCubeMesh.FACES[face].corners()[source];
|
||||
final int base = data.position();
|
||||
data.putFloat(base, corners[index * 3] * SCALE);
|
||||
data.putFloat(base + 4, corners[index * 3 + 1] * SCALE);
|
||||
data.putFloat(base + 8, corners[index * 3 + 2] * SCALE);
|
||||
data.putInt(base + 12, 0xFFFFFFFF);
|
||||
data.putFloat(base + 16, uv[(source == 0 || source == 3) ? 2 : 0]);
|
||||
data.putFloat(base + 20, uv[source >= 2 ? 3 : 1]);
|
||||
data.putFloat(base + 24, BRIGHTNESS);
|
||||
data.putFloat(base + 28, BRIGHTNESS);
|
||||
data.putFloat(base + 32, spec.mirror() ? -normal[0] : normal[0]);
|
||||
data.putFloat(base + 36, normal[1]);
|
||||
data.putFloat(base + 40, normal[2]);
|
||||
data.position(base + REF_STRIDE);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.flip();
|
||||
return data;
|
||||
}
|
||||
|
||||
private static int uploadQuads(int vbo, Spec[] specs, boolean lightmap) {
|
||||
final ByteBuffer data = buildQuadData(specs);
|
||||
if (vbo == 0) vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
|
||||
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, REF_STRIDE, 0);
|
||||
FfpFixture.attrib(1, 4, GL11.GL_UNSIGNED_BYTE, true, REF_STRIDE, 12);
|
||||
FfpFixture.attrib(2, 2, GL11.GL_FLOAT, false, REF_STRIDE, 16);
|
||||
if (lightmap) {
|
||||
FfpFixture.attrib(3, 2, GL11.GL_FLOAT, false, REF_STRIDE, 24);
|
||||
} else {
|
||||
GLStateManager.glDisableVertexAttribArray(3);
|
||||
}
|
||||
FfpFixture.attrib(4, 3, GL11.GL_FLOAT, false, REF_STRIDE, 32);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
return vbo;
|
||||
}
|
||||
|
||||
public static void drawReference(Spec[] specs, boolean lightmap) {
|
||||
if (referenceVao == 0) referenceVao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(referenceVao);
|
||||
referenceVbo = uploadQuads(referenceVbo, specs, lightmap);
|
||||
VAOManager.setCurrentVertexFlags(UnitCubeMesh.VERTEX_FLAGS | (lightmap ? VertexFlags.BRIGHTNESS_BIT : 0));
|
||||
|
||||
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, specs.length * 24);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
private static float[] corners(Spec spec) {
|
||||
final float minX = spec.x() - spec.inflate();
|
||||
final float minY = spec.y() - spec.inflate();
|
||||
final float minZ = spec.z() - spec.inflate();
|
||||
final float maxX = (spec.x() + spec.sizeX()) + spec.inflate();
|
||||
final float maxY = (spec.y() + spec.sizeY()) + spec.inflate();
|
||||
final float maxZ = (spec.z() + spec.sizeZ()) + spec.inflate();
|
||||
final float lowX = spec.mirror() ? maxX : minX;
|
||||
final float highX = spec.mirror() ? minX : maxX;
|
||||
final float[] out = new float[UnitCubeMesh.CORNERS.length * 3];
|
||||
for (int index = 0; index < UnitCubeMesh.CORNERS.length; index++) {
|
||||
final float[] corner = UnitCubeMesh.CORNERS[index];
|
||||
out[index * 3] = corner[0] > 0f ? highX : lowX;
|
||||
out[index * 3 + 1] = corner[1] > 0f ? maxY : minY;
|
||||
out[index * 3 + 2] = corner[2] > 0f ? maxZ : minZ;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static float[] faceRect(Spec spec, int face) {
|
||||
final int i = spec.texU();
|
||||
final int j = spec.texV();
|
||||
final int k = spec.sizeX();
|
||||
final int l = spec.sizeY();
|
||||
final int m = spec.sizeZ();
|
||||
final int[] rect = switch (face) {
|
||||
case 0 -> new int[] { i + m + k, j + m, i + m + k + m, j + m + l };
|
||||
case 1 -> new int[] { i, j + m, i + m, j + m + l };
|
||||
case 2 -> new int[] { i + m, j, i + m + k, j + m };
|
||||
case 3 -> new int[] { i + m + k, j + m, i + m + k + k, j };
|
||||
case 4 -> new int[] { i + m, j + m, i + m + k, j + m + l };
|
||||
default -> new int[] { i + m + k + m, j + m, i + m + k + m + k, j + m + l };
|
||||
};
|
||||
return new float[] {
|
||||
rect[0] / (float) TEX_WIDTH, rect[1] / (float) TEX_HEIGHT,
|
||||
rect[2] / (float) TEX_WIDTH, rect[3] / (float) TEX_HEIGHT,
|
||||
};
|
||||
}
|
||||
|
||||
private static int instanceVbo;
|
||||
|
||||
public static void drawInstanced(Spec[] specs) {
|
||||
final ByteBuffer records = BufferUtils.createByteBuffer(specs.length * CubeInstancedAttribs.STRIDE);
|
||||
final long records0 = memAddress0(records);
|
||||
for (int index = 0; index < specs.length; index++) {
|
||||
final Spec spec = specs[index];
|
||||
final CubeParams cube = CubeParams.of(TEX_WIDTH, TEX_HEIGHT, spec.mirror(), spec.texU(), spec.texV(), spec.x(), spec.y(), spec.z(), spec.sizeX(), spec.sizeY(), spec.sizeZ(), spec.inflate());
|
||||
if (cube == null) {
|
||||
throw new AssertionError("spec " + index + " is not cube instanceable");
|
||||
}
|
||||
final long ptr = records0 + (long) index * CubeInstancedAttribs.STRIDE;
|
||||
cube.writeRows(ptr, modelView, 0, SCALE);
|
||||
InstancedAttribs.writeTail(ptr, 0xFFFFFFFF, 0, 0L);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_LIGHTMAP_SCALE, BRIGHTNESS);
|
||||
memPutFloat(ptr + CubeInstancedAttribs.OFFSET_LIGHTMAP_SCALE + 4, BRIGHTNESS);
|
||||
cube.writeTexture(ptr);
|
||||
}
|
||||
records.position(0).limit(specs.length * CubeInstancedAttribs.STRIDE);
|
||||
|
||||
GLStateManager.glBindVertexArray(UnitCubeMesh.vao());
|
||||
VAOManager.setCurrentVertexFlags(UnitCubeMesh.VERTEX_FLAGS);
|
||||
if (instanceVbo == 0) instanceVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, records, GL15.GL_STREAM_DRAW);
|
||||
CubeInstancedAttribs.pointInstanceAttribs(0L);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
|
||||
GLStateManager.ffpInstancing = Instancing.CUBE;
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_QUADS, 0, UnitCubeMesh.VERTEX_COUNT, specs.length);
|
||||
GLStateManager.ffpInstancing = Instancing.NONE;
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
public static void assertPixelParity(int[] reference, int[] instanced, int size, int background, IntFunction<String> describe) {
|
||||
FfpFixture.assertPixelParity("instanced cubes", reference, instanced, size, background, 8, 1, describe);
|
||||
}
|
||||
|
||||
public static void deleteResources() {
|
||||
if (instanceVbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(instanceVbo);
|
||||
instanceVbo = 0;
|
||||
}
|
||||
if (referenceVbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(referenceVbo);
|
||||
referenceVbo = 0;
|
||||
}
|
||||
if (referenceVao != 0) {
|
||||
GLStateManager.glDeleteVertexArrays(referenceVao);
|
||||
referenceVao = 0;
|
||||
}
|
||||
if (netTexture != 0) {
|
||||
GLStateManager.glDeleteTextures(netTexture);
|
||||
netTexture = 0;
|
||||
}
|
||||
if (lightmapTexture != 0) {
|
||||
GLStateManager.glDeleteTextures(lightmapTexture);
|
||||
lightmapTexture = 0;
|
||||
}
|
||||
UnitCubeMesh.delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public final class FfpFixture {
|
||||
|
||||
private FfpFixture() {}
|
||||
|
||||
public static final class IntegerInstances {
|
||||
|
||||
private static final int INSTANCES = 3;
|
||||
|
||||
private static final String VS_HEAD = """
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 a_Position;
|
||||
layout(location = 5) in vec4 a_InstRow0;
|
||||
layout(location = 6) in vec4 a_InstRow1;
|
||||
layout(location = 7) in vec4 a_InstRow2;
|
||||
layout(location = 11) in ivec4 iris_Entity;
|
||||
flat out vec3 v_Color;
|
||||
void main() {
|
||||
""";
|
||||
|
||||
private static final String VS_TAIL = """
|
||||
gl_Position = instMV * vec4(a_Position, 1.0);
|
||||
if (iris_Entity.x == -1) { v_Color = vec3(1.0, 0.0, 0.0); }
|
||||
else if (iris_Entity.x == 7) { v_Color = vec3(0.0, 1.0, 0.0); }
|
||||
else { v_Color = vec3(0.0, 0.0, 1.0); }
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String VS = VS_HEAD + " mat4 instMV = " + InstancedGlslHelpers.mat4FromRows("a_InstRow0", "a_InstRow1", "a_InstRow2") + ";\n" + VS_TAIL;
|
||||
|
||||
private static final String FS = """
|
||||
#version 330 core
|
||||
flat in vec3 v_Color;
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(v_Color, 1.0); }
|
||||
""";
|
||||
|
||||
private static int program;
|
||||
private static int vao;
|
||||
private static int templateVbo;
|
||||
private static int instanceVbo;
|
||||
|
||||
private IntegerInstances() {}
|
||||
|
||||
public static void build(float spacing) {
|
||||
program = buildProgram();
|
||||
GLStateManager.glUseProgram(program);
|
||||
|
||||
buildTemplate();
|
||||
buildInstances(new float[] { -spacing, 0.0f, spacing }, new int[] { -1, 7, 42 });
|
||||
}
|
||||
|
||||
public static void draw() {
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_QUADS, 0, 4, INSTANCES);
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
GLStateManager.glUseProgram(0);
|
||||
if (program != 0) { GLStateManager.glDeleteProgram(program); program = 0; }
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
if (templateVbo != 0) { GLStateManager.glDeleteBuffers(templateVbo); templateVbo = 0; }
|
||||
if (instanceVbo != 0) { GLStateManager.glDeleteBuffers(instanceVbo); instanceVbo = 0; }
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
if (vao != 0) { GLStateManager.glDeleteVertexArrays(vao); vao = 0; }
|
||||
}
|
||||
|
||||
private static void buildTemplate() {
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
final ByteBuffer quad = BufferUtils.createByteBuffer(4 * 12);
|
||||
putVertex(quad, -0.15f, -0.8f);
|
||||
putVertex(quad, 0.15f, -0.8f);
|
||||
putVertex(quad, 0.15f, 0.8f);
|
||||
putVertex(quad, -0.15f, 0.8f);
|
||||
quad.flip();
|
||||
|
||||
templateVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, templateVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, quad, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0L);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
private static void putVertex(ByteBuffer buf, float x, float y) {
|
||||
final int base = buf.position();
|
||||
buf.putFloat(base, x).putFloat(base + 4, y).putFloat(base + 8, 0f);
|
||||
buf.position(base + 12);
|
||||
}
|
||||
|
||||
private static void buildInstances(float[] translateX, int[] entities) {
|
||||
final ByteBuffer instances = BufferUtils.createByteBuffer(entities.length * InstancedAttribs.STRIDE);
|
||||
final long base0 = memAddress0(instances);
|
||||
for (int i = 0; i < entities.length; i++) {
|
||||
final long ptr = base0 + (long) i * InstancedAttribs.STRIDE;
|
||||
InstancedAttribs.writeHead(ptr, translationMatrix(translateX[i]), 0, 0xFFFFFFFF, 0, InstancedAttribs.packEntityInfo(entities[i], 0, 0));
|
||||
}
|
||||
instances.position(0).limit(entities.length * InstancedAttribs.STRIDE);
|
||||
|
||||
instanceVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, instances, GL15.GL_STATIC_DRAW);
|
||||
InstancedAttribs.enableHeadArrays();
|
||||
InstancedAttribs.pointTemplate(0L);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
private static int buildProgram() {
|
||||
final int vs = compile(GL20.GL_VERTEX_SHADER, VS);
|
||||
final int fs = compile(GL20.GL_FRAGMENT_SHADER, FS);
|
||||
final int p = GLStateManager.glCreateProgram();
|
||||
GLStateManager.glAttachShader(p, vs);
|
||||
GLStateManager.glAttachShader(p, fs);
|
||||
GLStateManager.glLinkProgram(p);
|
||||
assertNotEquals(0, GLStateManager.glGetProgrami(p, GL20.GL_LINK_STATUS), () -> "test program must link: " + GLStateManager.glGetProgramInfoLog(p, 4096));
|
||||
GLStateManager.glDeleteShader(vs);
|
||||
GLStateManager.glDeleteShader(fs);
|
||||
return p;
|
||||
}
|
||||
|
||||
private static int compile(int type, String source) {
|
||||
final int shader = GLStateManager.glCreateShader(type);
|
||||
GLStateManager.glShaderSource(shader, source);
|
||||
GLStateManager.glCompileShader(shader);
|
||||
if (GLStateManager.glGetShaderi(shader, GL20.GL_COMPILE_STATUS) == 0) {
|
||||
fail("test shader failed to compile:\n" + GLStateManager.glGetShaderInfoLog(shader, 4096) + "\n\n" + source);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
}
|
||||
|
||||
public static void attrib(int loc, int size, int type, boolean normalized, int stride, int offset) {
|
||||
GLStateManager.glEnableVertexAttribArray(loc);
|
||||
GLStateManager.glVertexAttribPointer(loc, size, type, normalized, stride, offset);
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
GLStateManager.glClearColor(0f, 0f, 0f, 0f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
public static int[] readRegion(int size) {
|
||||
final ByteBuffer pixels = BufferUtils.createByteBuffer(size * size * 4);
|
||||
GLStateManager.glReadPixels(0, 0, size, size, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
|
||||
final int[] out = new int[size * size];
|
||||
for (int i = 0; i < out.length; i++) {
|
||||
out[i] = pixels.getInt(i * 4);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static int[] readPixel(int x, int y) {
|
||||
final ByteBuffer pixel = BufferUtils.createByteBuffer(4);
|
||||
GLStateManager.glReadPixels(x, y, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel);
|
||||
return new int[] { pixel.get(0) & 0xFF, pixel.get(1) & 0xFF, pixel.get(2) & 0xFF, pixel.get(3) & 0xFF };
|
||||
}
|
||||
|
||||
public static int solidTexture(int r, int g, int b, int a) {
|
||||
final int id = GLStateManager.glGenTextures();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
||||
final ByteBuffer texels = BufferUtils.createByteBuffer(2 * 2 * 4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
texels.put((byte) r).put((byte) g).put((byte) b).put((byte) a);
|
||||
}
|
||||
texels.flip();
|
||||
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 2, 2, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
|
||||
return id;
|
||||
}
|
||||
|
||||
public static float[] translationMatrix(float x) {
|
||||
return new float[] {
|
||||
1f, 0f, 0f, 0f,
|
||||
0f, 1f, 0f, 0f,
|
||||
0f, 0f, 1f, 0f,
|
||||
x, 0f, 0f, 1f,
|
||||
};
|
||||
}
|
||||
|
||||
public static void assertPixelParity(String subject, int[] reference, int[] candidate, int size, int background, int minCoverageDivisor, int channelTolerance, IntFunction<String> describe) {
|
||||
int covered = 0;
|
||||
for (int pixel : reference) {
|
||||
if (pixel != background) covered++;
|
||||
}
|
||||
final int coveredPixels = covered;
|
||||
assertTrue(coveredPixels > reference.length / minCoverageDivisor, () -> "reference draw covered only " + coveredPixels + " of " + reference.length + " pixels");
|
||||
|
||||
int mismatches = 0;
|
||||
int firstMismatch = -1;
|
||||
for (int i = 0; i < reference.length; i++) {
|
||||
if (!withinTolerance(reference[i], candidate[i], channelTolerance)) {
|
||||
mismatches++;
|
||||
if (firstMismatch < 0) firstMismatch = i;
|
||||
}
|
||||
}
|
||||
final int index = firstMismatch;
|
||||
final int differing = mismatches;
|
||||
assertEquals(0, mismatches, () -> subject + " differ from baked quads in " + differing + " of " + reference.length + " pixels, first at (" + (index % size) + "," + (index / size) + "): reference " + describe.apply(reference[index]) + " instanced " + describe.apply(candidate[index]));
|
||||
}
|
||||
|
||||
private static boolean withinTolerance(int a, int b, int tolerance) {
|
||||
if (tolerance == 0) return a == b;
|
||||
for (int shift = 0; shift <= 24; shift += 8) {
|
||||
final int ca = (a >> shift) & 0xFF;
|
||||
final int cb = (b >> shift) & 0xFF;
|
||||
if (Math.abs(ca - cb) > tolerance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void assertOverlayFootprint(int[] base, int[] overlay, int background, String overlayName) {
|
||||
int covered = 0;
|
||||
for (int i = 0; i < base.length; i++) {
|
||||
final int index = i;
|
||||
if (base[i] != background) {
|
||||
covered++;
|
||||
assertTrue(base[i] != overlay[i], () -> "pixel " + index + " covered by the base draw did not change under the " + overlayName + ": 0x" + Integer.toHexString(overlay[index]));
|
||||
} else {
|
||||
assertEquals(base[i], overlay[i], () -> "pixel " + index + " outside the base footprint must not be touched by the " + overlayName);
|
||||
}
|
||||
}
|
||||
final int coveredPixels = covered;
|
||||
assertTrue(coveredPixels > base.length / 8, "base draw covered only " + coveredPixels + " of " + base.length + " pixels");
|
||||
}
|
||||
|
||||
public static void resetFfpState() {
|
||||
GLStateManager.ffpInstancing = Instancing.NONE;
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
if (sm.isActive()) sm.deactivate();
|
||||
sm.disable();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
|
||||
GLStateManager.glDisable(GL11.GL_BLEND);
|
||||
GLStateManager.glDepthMask(true);
|
||||
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
|
||||
GLStateManager.glDisable(GL11.GL_DEPTH_TEST);
|
||||
GLStateManager.glDisable(GL11.GL_CULL_FACE);
|
||||
GLStateManager.glDisable(GL11.GL_ALPHA_TEST);
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
|
||||
public final class ParticleParityFixture {
|
||||
|
||||
public record Particle(float cx, float cy, float cz, float half, float u0, float v0, float u1, float v1, int colorABGR) {}
|
||||
|
||||
public record Rotation(float x, float xz, float z, float yz, float xy) {
|
||||
public static Rotation of(float yawDeg, float pitchDeg, boolean thirdPersonFront) {
|
||||
final float sign = thirdPersonFront ? -1f : 1f;
|
||||
final float yaw = (float) Math.toRadians(yawDeg);
|
||||
final float pitch = (float) Math.toRadians(pitchDeg);
|
||||
final float rotX = (float) Math.cos(yaw) * sign;
|
||||
final float rotZ = (float) Math.sin(yaw) * sign;
|
||||
final float rotYZ = -rotZ * (float) Math.sin(pitch) * sign;
|
||||
final float rotXY = rotX * (float) Math.sin(pitch) * sign;
|
||||
final float rotXZ = (float) Math.cos(pitch);
|
||||
return new Rotation(rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
}
|
||||
|
||||
public float offsetX(float a, float b) {
|
||||
return a * x + b * yz;
|
||||
}
|
||||
|
||||
public float offsetY(float b) {
|
||||
return b * xz;
|
||||
}
|
||||
|
||||
public float offsetZ(float a, float b) {
|
||||
return a * z + b * xy;
|
||||
}
|
||||
}
|
||||
|
||||
private static final float[] CORNER_A = { -1.0f, -1.0f, 1.0f, 1.0f };
|
||||
private static final float[] CORNER_B = { -1.0f, 1.0f, 1.0f, -1.0f };
|
||||
|
||||
private static final int REF_STRIDE = 24;
|
||||
|
||||
private static int referenceVao;
|
||||
private static int referenceVbo;
|
||||
private static int instanceVbo;
|
||||
private static int checkerTexture;
|
||||
|
||||
private ParticleParityFixture() {}
|
||||
|
||||
public static float cornerA(int corner) {
|
||||
return CORNER_A[corner];
|
||||
}
|
||||
|
||||
public static float cornerB(int corner) {
|
||||
return CORNER_B[corner];
|
||||
}
|
||||
|
||||
public static Particle[] particles() {
|
||||
return new Particle[] {
|
||||
new Particle(-0.75f, 0.6f, 0f, 0.10f, 0f, 0f, 1f, 1f, 0xFF0000FF),
|
||||
new Particle(-0.45f, -0.55f, 0f, 0.16f, 0f, 0f, 1f, 1f, 0xFF00FF00),
|
||||
new Particle(-0.15f, 0.2f, 0f, 0.07f, 0f, 0f, 1f, 1f, 0xFFFF0000),
|
||||
new Particle(0.1f, 0.75f, 0f, -0.12f, 0f, 0f, 1f, 1f, 0xFF00FFFF),
|
||||
new Particle(0.35f, -0.3f, 0f, 0.20f, 0f, 0f, 1f, 1f, 0xFFFF00FF),
|
||||
new Particle(0.6f, 0.45f, 0f, -0.08f, 0f, 0f, 1f, 1f, 0xFFFFFF00),
|
||||
new Particle(0.8f, -0.7f, 0f, 0.13f, 0f, 0f, 1f, 1f, 0xFFFFFFFF),
|
||||
new Particle(-0.8f, -0.1f, 0f, 0.09f, 0f, 0f, 1f, 1f, 0xFF0080FF),
|
||||
};
|
||||
}
|
||||
|
||||
private static int createCheckerTexture() {
|
||||
final int id = GLStateManager.glGenTextures();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, id);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
||||
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
||||
final ByteBuffer texels = BufferUtils.createByteBuffer(2 * 2 * 4);
|
||||
putTexel(texels, 255, 32, 32);
|
||||
putTexel(texels, 32, 255, 32);
|
||||
putTexel(texels, 32, 32, 255);
|
||||
putTexel(texels, 255, 255, 32);
|
||||
texels.flip();
|
||||
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 2, 2, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, texels);
|
||||
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
|
||||
return id;
|
||||
}
|
||||
|
||||
private static void putTexel(ByteBuffer buf, int r, int g, int b) {
|
||||
buf.put((byte) r).put((byte) g).put((byte) b).put((byte) 255);
|
||||
}
|
||||
|
||||
public static void setupScene(boolean textureMatrix) {
|
||||
GLStateManager.glDisable(GL11.GL_FOG);
|
||||
GLStateManager.glDisable(GL11.GL_BLEND);
|
||||
GLStateManager.glDisable(GL11.GL_ALPHA_TEST);
|
||||
GLStateManager.glDisable(GL11.GL_DEPTH_TEST);
|
||||
GLStateManager.glDisable(GL11.GL_CULL_FACE);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHTING);
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glLoadIdentity();
|
||||
|
||||
checkerTexture = createCheckerTexture();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, checkerTexture);
|
||||
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
|
||||
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
|
||||
GLStateManager.glLoadIdentity();
|
||||
if (textureMatrix) {
|
||||
GLStateManager.glTranslatef(0.1f, 0.1f, 0.0f);
|
||||
GLStateManager.glScalef(0.8f, 0.8f, 1.0f);
|
||||
}
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
}
|
||||
|
||||
private static ByteBuffer buildReferenceData(Particle[] particles, float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
final ByteBuffer data = BufferUtils.createByteBuffer(particles.length * 4 * REF_STRIDE);
|
||||
for (Particle p : particles) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
final float a = cornerA(i);
|
||||
final float b = cornerB(i);
|
||||
final float ox = a * rotX + b * rotYZ;
|
||||
final float oy = b * rotXZ;
|
||||
final float oz = a * rotZ + b * rotXY;
|
||||
final float u = a < 0f ? p.u0() : p.u1();
|
||||
final float v = b < 0f ? p.v0() : p.v1();
|
||||
final int base = data.position();
|
||||
data.putFloat(base, p.cx() + ox * p.half());
|
||||
data.putFloat(base + 4, p.cy() + oy * p.half());
|
||||
data.putFloat(base + 8, p.cz() + oz * p.half());
|
||||
data.putInt(base + 12, p.colorABGR());
|
||||
data.putFloat(base + 16, u);
|
||||
data.putFloat(base + 20, v);
|
||||
data.position(base + REF_STRIDE);
|
||||
}
|
||||
}
|
||||
data.flip();
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void drawReference(Particle[] particles, float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
final ByteBuffer data = buildReferenceData(particles, rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
|
||||
if (referenceVao == 0) referenceVao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(referenceVao);
|
||||
if (referenceVbo == 0) referenceVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, referenceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
|
||||
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, REF_STRIDE, 0);
|
||||
FfpFixture.attrib(1, 4, GL11.GL_UNSIGNED_BYTE, true, REF_STRIDE, 12);
|
||||
FfpFixture.attrib(2, 2, GL11.GL_FLOAT, false, REF_STRIDE, 16);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
VAOManager.setCurrentVertexFlags(VertexFlags.COLOR_BIT | VertexFlags.TEXTURE_BIT);
|
||||
|
||||
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, particles.length * 4);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
public static void drawInstanced(Particle[] particles, float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
|
||||
ParticleQuadMesh.update(rotX, rotXZ, rotZ, rotYZ, rotXY);
|
||||
|
||||
final ByteBuffer records = BufferUtils.createByteBuffer(particles.length * ParticleInstancedAttribs.STRIDE);
|
||||
final long records0 = memAddress0(records);
|
||||
for (int i = 0; i < particles.length; i++) {
|
||||
final Particle p = particles[i];
|
||||
final long ptr = records0 + (long) i * ParticleInstancedAttribs.STRIDE;
|
||||
ParticleInstancedAttribs.writeInstance(ptr, p.cx(), p.cy(), p.cz(), p.half(), p.u0(), p.v0(), p.u1(), p.v1(), p.colorABGR(), 0);
|
||||
}
|
||||
records.position(0).limit(particles.length * ParticleInstancedAttribs.STRIDE);
|
||||
|
||||
GLStateManager.glBindVertexArray(ParticleQuadMesh.vao());
|
||||
VAOManager.setCurrentVertexFlags(ParticleQuadMesh.VERTEX_FLAGS);
|
||||
if (instanceVbo == 0) instanceVbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, instanceVbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, records, GL15.GL_STREAM_DRAW);
|
||||
ParticleInstancedAttribs.pointInstanceAttribs(0L);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
|
||||
GLStateManager.ffpInstancing = Instancing.PARTICLE;
|
||||
GLStateManager.glDrawArraysInstanced(GL11.GL_QUADS, 0, ParticleQuadMesh.VERTEX_COUNT, particles.length);
|
||||
GLStateManager.ffpInstancing = Instancing.NONE;
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
public static void assertPixelParity(int[] reference, int[] instanced, int size, int background, IntFunction<String> describe) {
|
||||
FfpFixture.assertPixelParity("instanced particles", reference, instanced, size, background, 16, 1, describe);
|
||||
}
|
||||
|
||||
public static void deleteResources() {
|
||||
if (instanceVbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(instanceVbo);
|
||||
instanceVbo = 0;
|
||||
}
|
||||
if (referenceVbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(referenceVbo);
|
||||
referenceVbo = 0;
|
||||
}
|
||||
if (referenceVao != 0) {
|
||||
GLStateManager.glDeleteVertexArrays(referenceVao);
|
||||
referenceVao = 0;
|
||||
}
|
||||
if (checkerTexture != 0) {
|
||||
GLStateManager.glDeleteTextures(checkerTexture);
|
||||
checkerTexture = 0;
|
||||
}
|
||||
ParticleQuadMesh.delete();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm.testutil;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.reflect.Fields;
|
||||
import com.gtnewhorizon.gtnhlib.reflect.Fields.LookupType;
|
||||
import sun.misc.Unsafe;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -99,6 +100,17 @@ public final class Reflect {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T allocate(Class<T> type) {
|
||||
try {
|
||||
final Field f = Unsafe.class.getDeclaredField("theUnsafe");
|
||||
f.setAccessible(true);
|
||||
return (T) ((Unsafe) f.get(null)).allocateInstance(type);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Method resolve(Class<?> owner, String name, Class<?>[] signature) throws NoSuchMethodException {
|
||||
for (Class<?> c = owner; c != null; c = c.getSuperclass()) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage: scripts/flyby-bench.sh <name> ffp|pack [flyby-capture.sh args...]
|
||||
# Env: FLYBY_SCENE, TIME_OF_DAY, QUICKPLAY_WORLD, TRACY_CAPTURE, SDLGPU, RENDER_DISTANCE.
|
||||
# Env: FLYBY_SCENE, FLYBY_ORIGIN, TIME_OF_DAY, QUICKPLAY_WORLD, TRACY_CAPTURE, SDLGPU, RENDER_DISTANCE.
|
||||
# Forces particles:0 for the run and restores it after exit; the game rewrites options.txt on quit.
|
||||
# A worktree needs tracy-client/src/main/resources/natives copied in.
|
||||
|
||||
@@ -40,6 +40,7 @@ fi
|
||||
|
||||
export FLYBY_SCENE="${FLYBY_SCENE:-scripts/flyby-scenes/entities-x8.txt}"
|
||||
export QUICKPLAY_WORLD="${QUICKPLAY_WORLD:-Flat World - No Mods}"
|
||||
if [ "$QUICKPLAY_WORLD" = "Flat World - No Mods" ]; then export FLYBY_ORIGIN="${FLYBY_ORIGIN-1509.5,-924.5,0}"; fi
|
||||
export TRACY_CAPTURE="${TRACY_CAPTURE:-$HOME/dev/mc/tracy/capture/build/tracy-capture}"
|
||||
export SDLGPU="${SDLGPU:-1}"
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#
|
||||
# FLYBY_SPEED - blocks per tick. (0.5 ~= creative flight).
|
||||
# FLYBY_PACING - UNCAPPED | CONFIGURED. Passed through when set.
|
||||
# FLYBY_ORIGIN - x,z[,yaw] flight start. Unset or empty starts where the player is.
|
||||
# QUICKPLAY_WORLD - save to auto-load (default: latest).
|
||||
# FLYBY_SCENE - server command file run at flyby start.
|
||||
# SDLGPU=0|1 - force OpenGL (0) or SDL-GPU (1); unset uses the build default.
|
||||
@@ -43,6 +44,9 @@ fi
|
||||
if [ -n "${FLYBY_PACING:-}" ]; then
|
||||
BACKEND_ARGS+=(-Dangelica.flyby.pacing="$FLYBY_PACING")
|
||||
fi
|
||||
if [ -n "${FLYBY_ORIGIN:-}" ]; then
|
||||
BACKEND_ARGS+=(-Dangelica.flyby.origin="$FLYBY_ORIGIN")
|
||||
fi
|
||||
|
||||
SCENE_ABS=""
|
||||
SCENE_ARGS=()
|
||||
|
||||
@@ -8,12 +8,14 @@ import sys
|
||||
CSVEXPORT = os.environ.get("TRACY_CSVEXPORT", os.path.expanduser("~/dev/mc/tracy/csvexport/build/tracy-csvexport"))
|
||||
|
||||
PLOTS = [
|
||||
"gl.drawCalls", "gl.listPlaybacks", "gl.streamDraws", "entitiesRendered", "blockEntitiesRendered", "tesr.modelParts", "tesr.retainedDraws", "tesr.cacheMisses", "tesr.instancedDraws", "tesr.instancedInstances",
|
||||
"tesr.streamedInstances", "tesr.liveFallbacks", "tesr.bail.hurtFlash", "tesr.bail.foreignProgram",
|
||||
"tesr.bail.material", "tesr.bail.texture", "sdl.renderPasses", "sdl.submits", "sdl.gpuWaitUs",
|
||||
"gl.drawCalls", "gl.listPlaybacks", "gl.streamDraws", "entitiesRendered", "blockEntitiesRendered", "tesr.modelParts", "tesr.retainedDraws", "tesr.cacheMisses", "tesr.instancedDraws", "tesr.instancedInstances", "tesr.cubeInstances",
|
||||
"tesr.streamedInstances", "tesr.texMatrixRuns", "tesr.liveFallbacks", "tesr.bail.foreignProgram",
|
||||
"tesr.bail.material", "tesr.bail.texture", "entity.shadowQuads", "entity.shadowDraws", "items.instanced", "items.fallback", "items.glintInstanced",
|
||||
"items.bail.ineligible", "items.bail.material", "items.bail.isbrh", "items.bail.blockState", "items.bail.notAllowed", "items.bail.template", "items.bail.queue",
|
||||
"particles.direct", "particles.captured", "particles.spilled", "particles.undecodable", "particles.draws", "sdl.renderPasses", "sdl.submits", "sdl.gpuWaitUs",
|
||||
"sdl.acquireWaitUs",
|
||||
]
|
||||
ZONES = ["entityModelParts", "tesrBatch", "tesrInstanced", "tesrOpaque"]
|
||||
ZONES = ["entityModelParts", "tesrBatch", "tesrInstanced", "entityLayerLoop", "tesrOpaque", "particlePass"]
|
||||
CLOCK = "gl.drawCalls"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# entities.txt at 8x density. Summons are relative to the player; start facing south (yaw 0).
|
||||
# entities.txt at 8x density. Summons are relative to the flight start; start facing south (yaw 0).
|
||||
|
||||
# gamerules
|
||||
difficulty 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Summons are relative to the player; start facing south (yaw 0).
|
||||
# Summons are relative to the flight start; start facing south (yaw 0).
|
||||
|
||||
# gamerules
|
||||
difficulty 1
|
||||
|
||||
@@ -1011,6 +1011,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
@Override public void drawArrays(int mode, int first, int count) {
|
||||
if (count <= 0) return;
|
||||
final FrameState f = frameManager.frame();
|
||||
if (!f.frameActive) { f.droppedDrawsThisFrame++; return; }
|
||||
final ContextState st = s();
|
||||
@@ -1290,14 +1291,26 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
return isProxyTarget(target) ? 0 : st.boundTextures[st.activeTextureUnit];
|
||||
}
|
||||
|
||||
private int requestedMipLevels(int glId) {
|
||||
return Math.max(1, resourceManager.getOrCreateTexSamplerState(glId).maxLevel + 1);
|
||||
}
|
||||
|
||||
private int fullMipLevels(int glId, int width, int height) {
|
||||
final int full = PixelOps.defaultMipLevels(width, height);
|
||||
final int maxLevel = resourceManager.getOrCreateTexSamplerState(glId).maxLevel;
|
||||
return maxLevel >= 0 ? Math.min(full, maxLevel + 1) : full;
|
||||
}
|
||||
|
||||
private boolean growForLevel(int glId, int level) {
|
||||
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId);
|
||||
final int wanted = meta != null
|
||||
? Math.max(level + 1, fullMipLevels(glId, meta.width(), meta.height()))
|
||||
: level + 1;
|
||||
return resourceManager.ensureTextureLevels(glId, wanted);
|
||||
}
|
||||
|
||||
private void ensureLevel0Storage(ContextState st, int glId, int target, int internalFormat, int width, int height, int depth, int format, boolean mipmapped) {
|
||||
final int numLevels;
|
||||
if (mipmapped) {
|
||||
final TextureSamplerState ss = resourceManager.getOrCreateTexSamplerState(glId);
|
||||
numLevels = ss.maxLevel >= 0 ? ss.maxLevel + 1 : PixelOps.defaultMipLevels(width, height);
|
||||
} else {
|
||||
numLevels = 1;
|
||||
}
|
||||
final int numLevels = mipmapped ? requestedMipLevels(glId) : 1;
|
||||
final boolean bgraSwizzle = format == GL12.GL_BGRA && (internalFormat == GL11.GL_RGBA || internalFormat == GL11.GL_RGBA8);
|
||||
final int targetSdlFormat = bgraSwizzle
|
||||
? SDLGPU.SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM
|
||||
@@ -1321,7 +1334,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
&& m.width() == w
|
||||
&& m.height() == h
|
||||
&& m.depth() == d
|
||||
&& m.levels() == levels
|
||||
&& m.levels() >= levels
|
||||
&& m.glTarget() == target
|
||||
&& m.glFormat() == glFormat
|
||||
&& m.sdlFormat() == sdlFormat;
|
||||
@@ -1338,6 +1351,8 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
if (level == 0) {
|
||||
ensureLevel0Storage(st, glId, target, internalFormat, width, height, 1, format, true);
|
||||
} else if (!growForLevel(glId, level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
textureOps.uploadTextureRegion(st, glId, resourceManager.getTextureMeta(glId), resourceManager.getTextureHandle(glId), pixels, 0, 0, width, height, level, format, type);
|
||||
@@ -1408,6 +1423,8 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
if (level == 0) {
|
||||
ensureLevel0Storage(st, glId, target, internalFormat, width, height, 1, format, true);
|
||||
} else if (!growForLevel(glId, level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (st.boundPixelUnpackBuffer == 0) return;
|
||||
@@ -1458,6 +1475,8 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
if (level == 0) {
|
||||
ensureLevel0Storage(cs, destGlId, target, internalFormat, width, height, 1, GL11.GL_NONE, true);
|
||||
} else if (!growForLevel(destGlId, level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
textureOps.copyTexSubImageImpl(cs, destGlId, level, 0, 0, x, y, width, height);
|
||||
@@ -1562,6 +1581,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
case GL12.GL_TEXTURE_MAX_LOD -> ss.maxLod = (float) param;
|
||||
case GL14.GL_TEXTURE_COMPARE_MODE -> ss.compareMode = param;
|
||||
case GL14.GL_TEXTURE_COMPARE_FUNC -> ss.compareFunc = param;
|
||||
case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> ss.maxAnisotropy = param;
|
||||
default -> { return; }
|
||||
}
|
||||
ss.sdlSampler = 0;
|
||||
@@ -3321,32 +3341,31 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
resourceManager.refreshTextureReferences(glId);
|
||||
}
|
||||
@Override public void generateTextureMipmap(int texture) {
|
||||
final long texHandle = resourceManager.getTextureHandle(texture);
|
||||
if (texHandle == 0 || frameManager.getCommandBuffer() == 0) return;
|
||||
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
|
||||
if (meta == null || meta.levels() <= 1) return;
|
||||
if (resourceManager.getTextureHandle(texture) == 0 || frameManager.getCommandBuffer() == 0) return;
|
||||
ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
|
||||
if (meta == null) return;
|
||||
// The texture was created with only the levels GL had defined, so grow it to the chain glGenerateMipmap fills.
|
||||
final int wanted = fullMipLevels(texture, meta.width(), meta.height());
|
||||
if (wanted > meta.levels()) {
|
||||
if (!resourceManager.ensureTextureLevels(texture, wanted)) return;
|
||||
meta = resourceManager.getTextureMeta(texture);
|
||||
if (meta == null) return;
|
||||
}
|
||||
if (meta.levels() <= 1) return;
|
||||
// SDL asserts if any pass (render or copy) is active when generating mipmaps.
|
||||
frameManager.endCopyPassIfActive();
|
||||
frameManager.endRenderPassIfActive();
|
||||
frameManager.noteMipGen();
|
||||
textureOps.clearPendingMipGen(texture);
|
||||
SDL_GenerateMipmapsForGPUTexture(frameManager.getCommandBuffer(), texHandle);
|
||||
SDL_GenerateMipmapsForGPUTexture(frameManager.getCommandBuffer(), resourceManager.getTextureHandle(texture));
|
||||
}
|
||||
@Override public void textureImage2DEXT(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
|
||||
if (texture == 0) return;
|
||||
final ContextState st = s();
|
||||
if (level == 0) {
|
||||
final TextureSamplerState ss = resourceManager.getOrCreateTexSamplerState(texture);
|
||||
final int numLevels = ss.maxLevel >= 0 ? ss.maxLevel + 1 : PixelOps.defaultMipLevels(width, height);
|
||||
textureOps.releaseTextureForRealloc(st, texture);
|
||||
if (format == GL12.GL_BGRA && (internalformat == GL11.GL_RGBA || internalformat == GL11.GL_RGBA8)) {
|
||||
resourceManager.createTextureWithSdlFormat(texture, target,
|
||||
SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
|
||||
internalformat, width, height, 1, numLevels);
|
||||
} else {
|
||||
resourceManager.createTexture(texture, target, internalformat, width, height, 1, numLevels);
|
||||
}
|
||||
resourceManager.refreshTextureReferences(texture);
|
||||
ensureLevel0Storage(st, texture, target, internalformat, width, height, 1, format, true);
|
||||
} else if (!growForLevel(texture, level)) {
|
||||
return;
|
||||
}
|
||||
textureOps.uploadTextureRegion(st, texture, resourceManager.getTextureMeta(texture), resourceManager.getTextureHandle(texture), pixels, 0, 0, width, height, level, format, type);
|
||||
}
|
||||
@@ -3611,6 +3630,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
case GL11.GL_MAX_TEXTURE_SIZE -> MAX_TEXTURE_SIZE;
|
||||
case GL12.GL_MAX_3D_TEXTURE_SIZE -> MAX_3D_TEXTURE_SIZE;
|
||||
case GL13.GL_MAX_CUBE_MAP_TEXTURE_SIZE -> MAX_CUBE_MAP_TEXTURE_SIZE;
|
||||
case EXTTextureFilterAnisotropic.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT -> (int) MAX_ANISOTROPY;
|
||||
default -> -1;
|
||||
};
|
||||
}
|
||||
@@ -3620,6 +3640,8 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
static final int MAX_TEXTURE_SIZE = 16384;
|
||||
static final int MAX_CUBE_MAP_TEXTURE_SIZE = 16384;
|
||||
static final int MAX_3D_TEXTURE_SIZE = 2048;
|
||||
/** D3D12 and Metal both cap anisotropy at 16, and every desktop Vulkan driver reports 16. */
|
||||
static final float MAX_ANISOTROPY = 16.0f;
|
||||
|
||||
private static int unknownGetInteger(int pname) {
|
||||
synchronized (UNKNOWN_GET_INTEGER_SEEN) {
|
||||
@@ -3634,11 +3656,22 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
@Override public void getInteger(int pname, IntBuffer params) {
|
||||
if (params.remaining() > 0) params.put(params.position(), getInteger(pname));
|
||||
}
|
||||
private static float unknownGetFloat(int pname) {
|
||||
synchronized (UNKNOWN_GET_FLOAT_SEEN) {
|
||||
if (UNKNOWN_GET_FLOAT_SEEN.add(pname)) {
|
||||
LOG.warn("getFloat: unhandled pname 0x{}, returning 0; callers reading this as a device limit will treat the feature as unavailable", Integer.toHexString(pname));
|
||||
}
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
private static final IntOpenHashSet UNKNOWN_GET_FLOAT_SEEN = new IntOpenHashSet();
|
||||
@Override public float getFloat(int pname) {
|
||||
return switch (pname) {
|
||||
case GL11.GL_LINE_WIDTH -> 1.0f;
|
||||
case GL11.GL_POINT_SIZE -> 1.0f;
|
||||
default -> 0.0f;
|
||||
case EXTTextureFilterAnisotropic.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT -> MAX_ANISOTROPY;
|
||||
default -> unknownGetFloat(pname);
|
||||
};
|
||||
}
|
||||
@Override public void getFloat(int pname, FloatBuffer params) {
|
||||
@@ -3937,6 +3970,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
@Override
|
||||
public void drawArraysInstanced(int mode, int first, int count, int primcount) {
|
||||
if (count <= 0 || primcount <= 0) return;
|
||||
if (!frameManager.isFrameActive()) return;
|
||||
final ContextState st = s();
|
||||
drawDispatch.setPrimitiveTypeForDraw(st, FormatMap.mapPrimitiveType(mode));
|
||||
|
||||
@@ -109,6 +109,7 @@ public final class DrawDispatch {
|
||||
}
|
||||
|
||||
public void issueIndexedDraw(ContextState st, long rp, int ebo, int indexType, int count, int instances, int firstIndex, int baseVertex) {
|
||||
if (count <= 0 || instances <= 0) return;
|
||||
final EBOSplitScanner.EboSplit[] full;
|
||||
if (st.primitiveRestartEnabled) {
|
||||
full = resourceManager.getOrScanSplits(ebo, indexType, st.primitiveRestartSentinel);
|
||||
|
||||
+27
-11
@@ -69,8 +69,11 @@ public final class PipelineApplier {
|
||||
private final IntOpenHashSet dummyVboZeroWarned = new IntOpenHashSet();
|
||||
private final IntOpenHashSet staleFboWarned = new IntOpenHashSet();
|
||||
private final LongOpenHashSet bogusScissorWarned = new LongOpenHashSet();
|
||||
private final LongOpenHashSet clampedScissorWarned = new LongOpenHashSet();
|
||||
private final IntOpenHashSet pushUniformsNullCbWarned = new IntOpenHashSet();
|
||||
|
||||
private final int[] scissorRect = new int[4];
|
||||
|
||||
public PipelineApplier(FrameManager frameManager, ResourceManager resourceManager, ShaderManager shaderManager, PipelineStore pipelineStore, FBOClearTracker fboClearTracker, PersistentBufferSync persistentSync, SamplerBinder samplerBinder, StorageTextureBinder storageTextureBinder, StorageBufferBinder storageBufferBinder) {
|
||||
this.frameManager = frameManager;
|
||||
this.resourceManager = resourceManager;
|
||||
@@ -461,27 +464,40 @@ public final class PipelineApplier {
|
||||
}
|
||||
|
||||
if (st.scissorDirty) {
|
||||
final long addr = st.cachedScissor.address();
|
||||
final int sx, sy, sw, sh;
|
||||
if (st.scissorEnabled) {
|
||||
sx = st.scissorX;
|
||||
sy = renderingToFbo ? st.scissorY : (fbo0Height(st) - st.scissorY - st.scissorH);
|
||||
sw = st.scissorW;
|
||||
sh = st.scissorH;
|
||||
MemoryAccess.putInt(addr + SDL_Rect.X, sx);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.Y, sy);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.W, sw);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.H, sh);
|
||||
if ((sw <= 0 || sh <= 0) && bogusScissorWarned.add(Hashing.packHiLo(sw, sh))) {
|
||||
LOG.warn("applyPipelineAndState: scissor degenerate (x={} y={} w={} h={}) - all fragments clipped; boundProgram={} boundFbo={} scissorEn={} src=[{},{},{},{}]", sx, sy, sw, sh, st.boundProgram, st.boundFboId, st.scissorEnabled, st.scissorX, st.scissorY, st.scissorW, st.scissorH);
|
||||
}
|
||||
} else {
|
||||
final int rectY = renderingToFbo ? (int) st.viewportY : (int) (fbo0Height(st) - st.viewportY - st.viewportH);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.X, (int) st.viewportX);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.Y, rectY);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.W, (int) st.viewportW);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.H, (int) st.viewportH);
|
||||
sx = (int) st.viewportX;
|
||||
sy = renderingToFbo ? (int) st.viewportY : (int) (fbo0Height(st) - st.viewportY - st.viewportH);
|
||||
sw = (int) st.viewportW;
|
||||
sh = (int) st.viewportH;
|
||||
}
|
||||
|
||||
final boolean haveFboSize = renderingToFbo && fboState != null;
|
||||
final int targetW = haveFboSize ? fboState.width : frameManager.getFbo0Width();
|
||||
final int targetH = haveFboSize ? fboState.height : frameManager.getFbo0Height();
|
||||
final boolean nonEmpty = ScissorClamp.clamp(sx, sy, sw, sh, targetW, targetH, scissorRect);
|
||||
final int cx = scissorRect[ScissorClamp.X];
|
||||
final int cy = scissorRect[ScissorClamp.Y];
|
||||
final int cw = scissorRect[ScissorClamp.W];
|
||||
final int ch = scissorRect[ScissorClamp.H];
|
||||
if ((cx != sx || cy != sy || cw != sw || ch != sh) && clampedScissorWarned.add(Hashing.packHiLo(st.boundProgram, st.boundFboId))) {
|
||||
LOG.warn("applyPipelineAndState: scissor (x={} y={} w={} h={}) is outside the {}x{} target - clamped to (x={} y={} w={} h={}); boundProgram={} boundFbo={} scissorEn={}", sx, sy, sw, sh, targetW, targetH, cx, cy, cw, ch, st.boundProgram, st.boundFboId, st.scissorEnabled);
|
||||
}
|
||||
if (!nonEmpty) return false;
|
||||
|
||||
final long addr = st.cachedScissor.address();
|
||||
MemoryAccess.putInt(addr + SDL_Rect.X, cx);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.Y, cy);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.W, cw);
|
||||
MemoryAccess.putInt(addr + SDL_Rect.H, ch);
|
||||
SDL_SetGPUScissor(rp, st.cachedScissor);
|
||||
st.scissorDirty = false;
|
||||
}
|
||||
@@ -535,7 +551,7 @@ public final class PipelineApplier {
|
||||
final long elemAddr = st.vboBindingsAddr + (long) i * bindingSize;
|
||||
if (vboHandle != 0) {
|
||||
MemoryAccess.putAddress(elemAddr + SDL_GPUBufferBinding.BUFFER, vboHandle);
|
||||
MemoryAccess.putInt(elemAddr + SDL_GPUBufferBinding.OFFSET, (int) vao.bindingOffset[b]);
|
||||
MemoryAccess.putInt(elemAddr + SDL_GPUBufferBinding.OFFSET, PipelineCache.vertexOffsetBase(vao.bindingOffset[b]));
|
||||
} else {
|
||||
MemoryAccess.putAddress(elemAddr + SDL_GPUBufferBinding.BUFFER, dummyVBO);
|
||||
MemoryAccess.putInt(elemAddr + SDL_GPUBufferBinding.OFFSET, i * 16);
|
||||
|
||||
+12
-1
@@ -378,6 +378,7 @@ public final class PipelineCache {
|
||||
| (((long) vao.attribType[i] & 0xFFFFL) << 16)
|
||||
| ((vao.attribNormalized[i] ? 1L : 0L) << 32)
|
||||
| ((vao.attribIsInteger[i] ? 1L : 0L) << 33)
|
||||
| (((long) vertexOffsetMisalign(vao.bindingOffset[b]) & 3L) << 34)
|
||||
| (((long) b & 0xFF) << 40)
|
||||
| (((long) vao.attribStride[i] & 0xFFFFL) << 48);
|
||||
final long w1 =
|
||||
@@ -662,6 +663,16 @@ public final class PipelineCache {
|
||||
|
||||
private static final int GL_TYPE_BASE = GL11.GL_BYTE;
|
||||
static final int INVALID_FMT = -1;
|
||||
|
||||
public static final int VERTEX_BUFFER_OFFSET_ALIGN = 4;
|
||||
|
||||
public static int vertexOffsetBase(long bindingOffset) {
|
||||
return (int) (bindingOffset & ~(VERTEX_BUFFER_OFFSET_ALIGN - 1));
|
||||
}
|
||||
|
||||
public static int vertexOffsetMisalign(long bindingOffset) {
|
||||
return (int) (bindingOffset & (VERTEX_BUFFER_OFFSET_ALIGN - 1));
|
||||
}
|
||||
private static final int[][] VTX_FMT = buildVertexFormatTable();
|
||||
|
||||
private static int[] fmtSame(int s1, int s2, int s3, int s4) {
|
||||
@@ -839,7 +850,7 @@ public final class PipelineCache {
|
||||
final int boundStride = vao.bindingStride[b];
|
||||
final int stride = boundStride != 0 ? boundStride : (vao.attribStride[i] != 0 ? vao.attribStride[i] : vao.attribSize[i] * GLTypes.sizeBytes(vao.attribType[i]));
|
||||
final int divisor = vao.bindingDivisor[b];
|
||||
final int relOff = vao.attribRelativeOffset[i];
|
||||
final int relOff = vao.attribRelativeOffset[i] + vertexOffsetMisalign(vao.bindingOffset[b]);
|
||||
final int attribAlign = GLTypes.sizeBytes(vao.attribType[i]);
|
||||
if (attribAlign > 0 && (stride % attribAlign) != 0) {
|
||||
LOG.warn("[PipelineCache] unaligned vertex stride: location={} stride={} type=0x{} size={} -- attribAddress will not satisfy format alignment; fix producer", i, stride, Integer.toHexString(vao.attribType[i]), vao.attribSize[i]);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
|
||||
|
||||
public final class ScissorClamp {
|
||||
|
||||
public static final int X = 0;
|
||||
public static final int Y = 1;
|
||||
public static final int W = 2;
|
||||
public static final int H = 3;
|
||||
|
||||
private ScissorClamp() {}
|
||||
|
||||
public static boolean clamp(int x, int y, int w, int h, int targetW, int targetH, int[] out) {
|
||||
if (targetW <= 0 || targetH <= 0) {
|
||||
out[X] = x;
|
||||
out[Y] = y;
|
||||
out[W] = w;
|
||||
out[H] = h;
|
||||
return w > 0 && h > 0;
|
||||
}
|
||||
final int x0 = Math.max(x, 0);
|
||||
final int y0 = Math.max(y, 0);
|
||||
final int x1 = (int) Math.min((long) x + w, targetW);
|
||||
final int y1 = (int) Math.min((long) y + h, targetH);
|
||||
out[X] = x0;
|
||||
out[Y] = y0;
|
||||
out[W] = Math.max(x1 - x0, 0);
|
||||
out[H] = Math.max(y1 - y0, 0);
|
||||
return out[W] > 0 && out[H] > 0;
|
||||
}
|
||||
}
|
||||
+36
-1
@@ -513,7 +513,7 @@ public final class ResourceManager {
|
||||
final int newUsage = reconcileUsageAndWarn(glId, meta.usage | requiredUsage);
|
||||
if ((meta.usage & newUsage) == newUsage) return oldHandle;
|
||||
|
||||
final long newHandle = createTexture(glId, meta.glTarget, meta.glFormat, meta.width, meta.height, meta.depth, meta.levels, newUsage);
|
||||
final long newHandle = createTextureWithSdlFormat(glId, meta.glTarget, meta.sdlFormat, meta.glFormat, meta.width, meta.height, meta.depth, meta.levels, newUsage);
|
||||
if (newHandle == 0) return oldHandle;
|
||||
if (oldHandle != 0 && !copyAllMips(oldHandle, newHandle, meta.glTarget, meta.width, meta.height, meta.depth, meta.levels)) {
|
||||
LOG.warn("[ResourceManager] texture {} usage promote 0x{} -> 0x{} had no copy pass available; restoring the populated texture and skipping the promote. Image load/store on it will not work this frame.", glId, Integer.toHexString(meta.usage), Integer.toHexString(newUsage));
|
||||
@@ -530,6 +530,41 @@ public final class ResourceManager {
|
||||
return newHandle;
|
||||
}
|
||||
|
||||
public boolean ensureTextureLevels(int glId, int levels) {
|
||||
final TextureMeta meta;
|
||||
final long oldHandle;
|
||||
if (fastRead()) {
|
||||
meta = textureMetas.get(glId);
|
||||
if (meta == null) return false;
|
||||
if (meta.levels >= levels) return true;
|
||||
oldHandle = textureHandles.get(glId);
|
||||
} else {
|
||||
rLock.lock();
|
||||
try {
|
||||
meta = textureMetas.get(glId);
|
||||
if (meta == null) return false;
|
||||
if (meta.levels >= levels) return true;
|
||||
oldHandle = textureHandles.get(glId);
|
||||
} finally {
|
||||
rLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
final long newHandle = createTextureWithSdlFormat(glId, meta.glTarget, meta.sdlFormat, meta.glFormat, meta.width, meta.height, meta.depth, levels, meta.usage);
|
||||
if (newHandle == 0) return false;
|
||||
if (oldHandle != 0 && !copyAllMips(oldHandle, newHandle, meta.glTarget, meta.width, meta.height, meta.depth, meta.levels)) {
|
||||
LOG.warn("[ResourceManager] texture {} mip grow {} -> {} had no copy pass available; keeping the populated texture. Levels past {} stay undefined this frame.", glId, meta.levels, levels, meta.levels - 1);
|
||||
releaseTextureDeferred(newHandle);
|
||||
restoreTextureHandle(glId, oldHandle, meta);
|
||||
return false;
|
||||
}
|
||||
|
||||
releaseTextureDeferred(oldHandle);
|
||||
refreshTextureReferences(glId);
|
||||
LOG.debug("Recreated texture {} with levels {} -> {} ({}x{})", glId, meta.levels, levels, meta.width, meta.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean copyAllMips(long srcTex, long dstTex, int glTarget, int width, int height, int depth, int levels) {
|
||||
final long cp = frameManager.ensureCopyPass();
|
||||
if (cp == 0) return false;
|
||||
|
||||
@@ -81,6 +81,7 @@ public final class TextureOps {
|
||||
case GL12.GL_TEXTURE_MAX_LOD -> ss.maxLod = (float) param;
|
||||
case GL14.GL_TEXTURE_COMPARE_MODE -> ss.compareMode = param;
|
||||
case GL14.GL_TEXTURE_COMPARE_FUNC -> ss.compareFunc = param;
|
||||
case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> ss.maxAnisotropy = param;
|
||||
default -> { return; }
|
||||
}
|
||||
ss.sdlSampler = 0;
|
||||
|
||||
+2
@@ -31,6 +31,8 @@ public final class PerFrameBlockInjector {
|
||||
private PerFrameBlockInjector() {}
|
||||
|
||||
public static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
if (normalize(perFrame) == null && normalize(perPass) == null) return source;
|
||||
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
|
||||
@@ -26,6 +26,8 @@ public final class SamplerStripper {
|
||||
private SamplerStripper() {}
|
||||
|
||||
public static String stripUnused(String source) {
|
||||
if (!source.contains("sampler")) return source;
|
||||
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
import com.gtnewhorizons.angelica.config.SystemProperties;
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ShaderWorkSubmitter;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
@@ -88,8 +89,14 @@ public final class ShaderManager {
|
||||
private static final int PREWARM_CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<PrewarmKey, PrewarmEntry> PREWARM_CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
private record TransformKey(String source, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {}
|
||||
private record TransformEntry(String source, Set<String> boolUniforms) {}
|
||||
private static final int TRANSFORM_CACHE_MAX = 256;
|
||||
private static final Object2ObjectLinkedOpenHashMap<TransformKey, TransformEntry> TRANSFORM_CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
public static void clearPrewarmCache() {
|
||||
synchronized (PREWARM_CACHE) { PREWARM_CACHE.clear(); }
|
||||
synchronized (TRANSFORM_CACHE) { TRANSFORM_CACHE.clear(); }
|
||||
}
|
||||
|
||||
private static PrewarmHit lookupPrewarm(String src, int glShaderType) {
|
||||
@@ -133,24 +140,35 @@ public final class ShaderManager {
|
||||
return;
|
||||
}
|
||||
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(raw, obj.type, "shader" + shader, true);
|
||||
obj.boolUniforms = pre != null ? pre.boolUniforms() : Set.of();
|
||||
String src = pre != null ? pre.rewrittenSource() : raw;
|
||||
if (obj.isVertex()) {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
final TransformKey transformKey = new TransformKey(raw, obj.type, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
TransformEntry transformed;
|
||||
synchronized (TRANSFORM_CACHE) {
|
||||
transformed = TRANSFORM_CACHE.getAndMoveToFirst(transformKey);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (obj.type == GL20.GL_VERTEX_SHADER || obj.type == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
if (transformed == null) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(raw, obj.type, "shader" + shader, true);
|
||||
String src = pre != null ? pre.rewrittenSource() : raw;
|
||||
if (obj.isVertex()) {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (obj.type == GL20.GL_VERTEX_SHADER || obj.type == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
transformed = new TransformEntry(src, pre != null ? pre.boolUniforms() : Set.of());
|
||||
synchronized (TRANSFORM_CACHE) {
|
||||
TRANSFORM_CACHE.putAndMoveToFirst(transformKey, transformed);
|
||||
while (TRANSFORM_CACHE.size() > TRANSFORM_CACHE_MAX) TRANSFORM_CACHE.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
obj.source = src;
|
||||
obj.boolUniforms = transformed.boolUniforms();
|
||||
obj.source = transformed.source();
|
||||
|
||||
final ShaderWorkSubmitter submitter = GLSMHooks.shaderWorkSubmitter;
|
||||
if (submitter != null) {
|
||||
final int shaderKind = shaderKindFor(obj);
|
||||
final int glType = obj.type;
|
||||
final String finalSrc = src;
|
||||
final String finalSrc = obj.source;
|
||||
obj.spirvFuture = submitter.submit(() -> {
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(finalSrc, shaderKind, "shader" + shader, SpirvCompiler.Options.vulkanForced460Core());
|
||||
if (r.spirv() != null && (glType == GL20.GL_VERTEX_SHADER || glType == GL20.GL_FRAGMENT_SHADER)) {
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
@Tag("glsm-sdl")
|
||||
@ExtendWith(GlsmSdlFrameExtension.class)
|
||||
class GlsmSdlDegenerateDrawTest {
|
||||
|
||||
private static final int RED = 0xFFFF0000;
|
||||
|
||||
@BeforeAll
|
||||
static void boot() {
|
||||
GlsmSdlHeadlessRig.boot();
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroCountDrawArraysLeavesTheFollowingDrawIntact() {
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 0);
|
||||
GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f);
|
||||
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(), RED, "after a zero-count drawArrays");
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeCountDrawArraysLeavesTheFollowingDrawIntact() {
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, -3);
|
||||
GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f);
|
||||
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(), RED, "after a negative-count drawArrays");
|
||||
}
|
||||
}
|
||||
+34
@@ -62,6 +62,13 @@ class GlsmSdlFbo0ViewportTest {
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
private static void bindFbo0WithFullViewport() {
|
||||
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
|
||||
GLStateManager.glViewport(0, 0, FBO0_W, FBO0_H);
|
||||
GLStateManager.glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
private static int createCopyTarget() {
|
||||
final int id = GLStateManager.glGenTextures();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
@@ -88,6 +95,33 @@ class GlsmSdlFbo0ViewportTest {
|
||||
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(0, FBO0_H - VIEW, VIEW, VIEW), BLUE, "above the viewport");
|
||||
}
|
||||
|
||||
@Test
|
||||
void scissorLargerThanFbo0StillCoversTheWholeTarget() {
|
||||
bindFbo0WithFullViewport();
|
||||
GLStateManager.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
try {
|
||||
GLStateManager.glScissor(0, 0, FBO0_W + 16, FBO0_H + 16);
|
||||
GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f);
|
||||
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(0, 0, FBO0_W, FBO0_H), RED, "inside an oversize scissor");
|
||||
} finally {
|
||||
GLStateManager.glDisable(GL11.GL_SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void scissorRunningPastTheRightEdgeClipsToTheTarget() {
|
||||
bindFbo0WithFullViewport();
|
||||
GLStateManager.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
try {
|
||||
GLStateManager.glScissor(FBO0_W - 8, 0, 32, FBO0_H);
|
||||
GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f);
|
||||
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(FBO0_W - 8, 0, 8, FBO0_H), RED, "inside the clipped scissor");
|
||||
GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(0, 0, FBO0_W - 8, FBO0_H), BLUE, "left of the scissor");
|
||||
} finally {
|
||||
GLStateManager.glDisable(GL11.GL_SCISSOR_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyTexSubImage2DFromAPartialViewportKeepsGlOrientation() {
|
||||
bindFbo0WithSmallViewport();
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.CubeParityFixture;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ParticleParityFixture;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -40,6 +45,14 @@ class GlsmSdlFfpTest {
|
||||
GlsmSdlHeadlessRig.endFrame();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanupInstancing() {
|
||||
GLStateManager.ffpInstancing = Instancing.NONE;
|
||||
CubeParityFixture.deleteResources();
|
||||
ParticleParityFixture.deleteResources();
|
||||
FfpFixture.IntegerInstances.delete();
|
||||
}
|
||||
|
||||
private static int createLightmapTexture() {
|
||||
final int id = GLStateManager.glGenTextures();
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
|
||||
@@ -146,9 +159,7 @@ class GlsmSdlFfpTest {
|
||||
GLStateManager.glCallList(list);
|
||||
assertTexelEverywhere("two units, glCallList");
|
||||
|
||||
GlsmSdlHeadlessRig.beginFrame();
|
||||
GlsmSdlHeadlessRig.bindTarget();
|
||||
GlsmSdlHeadlessRig.clearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
GlsmSdlHeadlessRig.beginFrameAndClear();
|
||||
|
||||
final IntBuffer lists = BufferUtils.createIntBuffer(1);
|
||||
lists.put(0, list);
|
||||
@@ -172,6 +183,54 @@ class GlsmSdlFfpTest {
|
||||
assertTrue(red < ((TEXEL >> 16) & 0xFF), () -> "lit quad is not attenuated: " + describe(center));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unitCubeMatchesBakedQuadsWithLightmap() {
|
||||
CubeParityFixture.setupScene(true, false);
|
||||
|
||||
final CubeParityFixture.Spec[] specs = CubeParityFixture.specs();
|
||||
|
||||
CubeParityFixture.drawReference(specs, true);
|
||||
final int[] reference = GlsmSdlHeadlessRig.readTarget();
|
||||
|
||||
GlsmSdlHeadlessRig.beginFrameAndClear();
|
||||
CubeParityFixture.drawInstanced(specs);
|
||||
final int[] instanced = GlsmSdlHeadlessRig.readTarget();
|
||||
|
||||
CubeParityFixture.assertPixelParity(reference, instanced, SIZE, 0xFF000000, GlsmSdlHeadlessRig::describe);
|
||||
}
|
||||
|
||||
@Test
|
||||
void particleFirstPersonRotationMatchesReference() {
|
||||
final ParticleParityFixture.Rotation rot = ParticleParityFixture.Rotation.of(35f, -12f, false);
|
||||
ParticleParityFixture.setupScene(false);
|
||||
|
||||
final ParticleParityFixture.Particle[] particles = ParticleParityFixture.particles();
|
||||
|
||||
ParticleParityFixture.drawReference(particles, rot.x(), rot.xz(), rot.z(), rot.yz(), rot.xy());
|
||||
final int[] reference = GlsmSdlHeadlessRig.readTarget();
|
||||
|
||||
GlsmSdlHeadlessRig.beginFrameAndClear();
|
||||
ParticleParityFixture.drawInstanced(particles, rot.x(), rot.xz(), rot.z(), rot.yz(), rot.xy());
|
||||
final int[] instanced = GlsmSdlHeadlessRig.readTarget();
|
||||
|
||||
ParticleParityFixture.assertPixelParity(reference, instanced, SIZE, 0xFF000000, GlsmSdlHeadlessRig::describe);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entityAttribSelectsColorPerInstance() {
|
||||
FfpFixture.IntegerInstances.build(0.6f);
|
||||
|
||||
GlsmSdlHeadlessRig.beginFrameAndClear();
|
||||
|
||||
FfpFixture.IntegerInstances.draw();
|
||||
|
||||
final int[] pixels = GlsmSdlHeadlessRig.readTarget();
|
||||
|
||||
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, 13, SIZE / 2, 0xFFFF0000, "entity -1 must render red");
|
||||
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, 32, SIZE / 2, 0xFF00FF00, "entity 7 must render green");
|
||||
GlsmSdlHeadlessRig.assertPixel(pixels, SIZE, 51, SIZE / 2, 0xFF0000FF, "entity 42 must render blue");
|
||||
}
|
||||
|
||||
private static void enableGuiStandardItemLighting() {
|
||||
GLStateManager.glPushMatrix();
|
||||
GLStateManager.glRotatef(-30.0f, 0.0f, 1.0f, 0.0f);
|
||||
|
||||
@@ -122,6 +122,12 @@ public final class GlsmSdlHeadlessRig {
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
|
||||
public static void beginFrameAndClear() {
|
||||
beginFrame();
|
||||
bindTarget();
|
||||
clearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
public static void beginFrameAndReset() {
|
||||
beginFrame();
|
||||
resetState();
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.glsm;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Tag("glsm-sdl")
|
||||
@ExtendWith(GlsmSdlFrameExtension.class)
|
||||
class GlsmSdlUnalignedAttribTest {
|
||||
|
||||
private static final int STRIDE = 24;
|
||||
private static final int FACTOR = 204;
|
||||
private static final int DECOY = 64;
|
||||
|
||||
private static final String VERT = """
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 a_Position;
|
||||
layout(location = 5) in float a_Factor;
|
||||
out float v_Factor;
|
||||
void main() {
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
v_Factor = a_Factor;
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String FRAG = """
|
||||
#version 330 core
|
||||
in float v_Factor;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = vec4(v_Factor, v_Factor, v_Factor, 1.0);
|
||||
}
|
||||
""";
|
||||
|
||||
private static int program;
|
||||
|
||||
@BeforeAll
|
||||
static void boot() {
|
||||
GlsmSdlHeadlessRig.boot();
|
||||
}
|
||||
|
||||
private static int buildProgram() {
|
||||
if (program != 0) return program;
|
||||
final int vs = GLStateManager.glCreateShader(GL20.GL_VERTEX_SHADER);
|
||||
GLStateManager.glShaderSource(vs, VERT);
|
||||
GLStateManager.glCompileShader(vs);
|
||||
assertTrue(GLStateManager.glGetShaderi(vs, GL20.GL_COMPILE_STATUS) != 0,
|
||||
() -> "vertex shader: " + GLStateManager.glGetShaderInfoLog(vs));
|
||||
|
||||
final int fs = GLStateManager.glCreateShader(GL20.GL_FRAGMENT_SHADER);
|
||||
GLStateManager.glShaderSource(fs, FRAG);
|
||||
GLStateManager.glCompileShader(fs);
|
||||
assertTrue(GLStateManager.glGetShaderi(fs, GL20.GL_COMPILE_STATUS) != 0,
|
||||
() -> "fragment shader: " + GLStateManager.glGetShaderInfoLog(fs));
|
||||
|
||||
final int p = GLStateManager.glCreateProgram();
|
||||
GLStateManager.glAttachShader(p, vs);
|
||||
GLStateManager.glAttachShader(p, fs);
|
||||
GLStateManager.glLinkProgram(p);
|
||||
assertTrue(GLStateManager.glGetProgrami(p, GL20.GL_LINK_STATUS) != 0, "program did not link");
|
||||
program = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
private static int redChannelForFactorAt(int factorOffset, int decoyOffset) {
|
||||
final int p = buildProgram();
|
||||
final int vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
final int vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
final ByteBuffer data = MemoryUtil.memAlloc(3 * STRIDE);
|
||||
try {
|
||||
for (int i = 0; i < 3 * STRIDE; i++) data.put(i, (byte) 0);
|
||||
final float[][] corners = {{-1.0f, -1.0f}, {3.0f, -1.0f}, {-1.0f, 3.0f}};
|
||||
for (int v = 0; v < 3; v++) {
|
||||
final int base = v * STRIDE;
|
||||
data.putFloat(base, corners[v][0]);
|
||||
data.putFloat(base + 4, corners[v][1]);
|
||||
data.putFloat(base + 8, 0.0f);
|
||||
if (decoyOffset >= 0) data.put(base + decoyOffset, (byte) DECOY);
|
||||
data.put(base + factorOffset, (byte) FACTOR);
|
||||
}
|
||||
data.position(0).limit(3 * STRIDE);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||
} finally {
|
||||
MemoryUtil.memFree(data);
|
||||
}
|
||||
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, STRIDE, 0L);
|
||||
GLStateManager.glEnableVertexAttribArray(5);
|
||||
GLStateManager.glVertexAttribPointer(5, 1, GL11.GL_UNSIGNED_BYTE, true, STRIDE, factorOffset);
|
||||
|
||||
GLStateManager.glUseProgram(p);
|
||||
try {
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
|
||||
} finally {
|
||||
GLStateManager.glUseProgram(0);
|
||||
}
|
||||
|
||||
final int[] pixels = GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
return (pixels[0] >> 16) & 0xFF;
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteAttribAtAnAlignedOffsetReadsItsOwnByte() {
|
||||
assertEquals(FACTOR, redChannelForFactorAt(20, -1), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteAttribAtAnUnalignedOffsetReadsItsOwnByte() {
|
||||
assertEquals(FACTOR, redChannelForFactorAt(23, 20), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void byteAttribAtEveryMisalignmentReadsItsOwnByte() {
|
||||
for (int offset = 20; offset <= 23; offset++) {
|
||||
final int decoy = offset == 20 ? -1 : 20;
|
||||
final int got = redChannelForFactorAt(offset, decoy);
|
||||
assertEquals(FACTOR, got, 1, "factor byte at offset " + offset + " read back as " + got);
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ScissorClampTest {
|
||||
|
||||
private final int[] out = new int[4];
|
||||
|
||||
@Test
|
||||
void rectInsideTheTargetIsUnchanged() {
|
||||
assertTrue(ScissorClamp.clamp(4, 8, 16, 32, 64, 96, out));
|
||||
assertArrayEquals(new int[]{4, 8, 16, 32}, out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overhangPastTheRightAndBottomEdgesShrinks() {
|
||||
assertTrue(ScissorClamp.clamp(0, 0, 1710, 960, 1708, 960, out));
|
||||
assertArrayEquals(new int[]{0, 0, 1708, 960}, out);
|
||||
|
||||
assertTrue(ScissorClamp.clamp(60, 90, 16, 32, 64, 96, out));
|
||||
assertArrayEquals(new int[]{60, 90, 4, 6}, out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeOriginShrinksTheRectInsteadOfShiftingIt() {
|
||||
assertTrue(ScissorClamp.clamp(-6, -10, 16, 32, 64, 96, out));
|
||||
assertArrayEquals(new int[]{0, 0, 10, 22}, out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rectStraddlingBothEdgesIsClampedToTheWholeTarget() {
|
||||
assertTrue(ScissorClamp.clamp(-8, -8, 128, 128, 64, 96, out));
|
||||
assertArrayEquals(new int[]{0, 0, 64, 96}, out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rectFullyOutsideTheTargetIsEmpty() {
|
||||
assertFalse(ScissorClamp.clamp(64, 0, 16, 32, 64, 96, out));
|
||||
assertArrayEquals(new int[]{64, 0, 0, 32}, out);
|
||||
|
||||
assertFalse(ScissorClamp.clamp(-40, 0, 16, 32, 64, 96, out));
|
||||
assertArrayEquals(new int[]{0, 0, 0, 32}, out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void degenerateSourceRectIsEmpty() {
|
||||
assertFalse(ScissorClamp.clamp(0, 0, 0, 32, 64, 96, out));
|
||||
assertFalse(ScissorClamp.clamp(0, 0, 16, -4, 64, 96, out));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownTargetSizeLeavesTheRectAlone() {
|
||||
assertTrue(ScissorClamp.clamp(-6, 4, 1710, 32, 0, 0, out));
|
||||
assertArrayEquals(new int[]{-6, 4, 1710, 32}, out);
|
||||
|
||||
assertFalse(ScissorClamp.clamp(0, 0, 0, 32, 0, 96, out));
|
||||
}
|
||||
|
||||
@Test
|
||||
void largeRectDoesNotOverflow() {
|
||||
assertTrue(ScissorClamp.clamp(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE, 64, 96, out));
|
||||
assertArrayEquals(new int[]{0, 0, 64, 96}, out);
|
||||
}
|
||||
}
|
||||
+8
@@ -14,6 +14,7 @@ import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
@@ -80,6 +81,13 @@ class PerFrameBlockInjectorTest {
|
||||
assertEquals(src, PerFrameBlockInjector.inject(src, new PerFrameUniformBlock(List.of()), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noBlocksReturnsTheSourceInstance() {
|
||||
final String src = "#version 330 core\nuniform float frameTimeCounter;\nvoid main() {}\n";
|
||||
assertSame(src, PerFrameBlockInjector.inject(src, null, null));
|
||||
assertSame(src, PerFrameBlockInjector.inject(src, new PerFrameUniformBlock(List.of()), new PerFrameUniformBlock(List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void memberShadowedByAPackDeclarationIsRenamedNotRedeclared() {
|
||||
final String out = PerFrameBlockInjector.inject("""
|
||||
|
||||
+2
-15
@@ -23,22 +23,9 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
private static String legacy(String source, int glShaderType) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : source;
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
private static void assertEquivalent(String label, String source, int glShaderType) {
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String legacyOut = legacy(source, glShaderType);
|
||||
final String legacyOut = ShaderTransformChain.run(source, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransforms(source, glShaderType);
|
||||
assertEquals(legacyOut, parseOnceOut, "single-parse vs chained pipeline diverged for " + label);
|
||||
@@ -46,7 +33,7 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
|
||||
private static void assertEquivalentIgnoringWhitespace(String label, String source, int glShaderType) {
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String legacyOut = legacy(source, glShaderType);
|
||||
final String legacyOut = ShaderTransformChain.run(source, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransforms(source, glShaderType);
|
||||
assertEquals(legacyOut.replaceAll("\\s+", " "), parseOnceOut.replaceAll("\\s+", " "), "single-parse vs chained pipeline diverged beyond whitespace for " + label);
|
||||
|
||||
+11
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class SdlSamplerStripperPreprocessorTest {
|
||||
@@ -104,6 +105,16 @@ class SdlSamplerStripperPreprocessorTest {
|
||||
assertFalse(out.contains("uniform sampler2D u_S"), "a comment mention must not keep the decl; output:\n" + out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sourceWithoutSamplersReturnsTheSourceInstance() {
|
||||
final String src = "#version 460 core\n"
|
||||
+ "in vec3 v_Position;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = vec4(v_Position, 1.0);\n"
|
||||
+ "}\n";
|
||||
assertSame(src, SamplerStripper.stripUnused(src));
|
||||
}
|
||||
|
||||
@Test
|
||||
void samplerOnlyInMacroBody_isPreserved() {
|
||||
final String src = "#version 460 core\n"
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ShaderSourceTransformCacheTest {
|
||||
|
||||
private static final String FRAG = """
|
||||
#version 460 core
|
||||
uniform sampler2D u_Unused;
|
||||
uniform sampler2D u_Used;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texture(u_Used, vec2(0.5));
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String VERT = """
|
||||
#version 460 core
|
||||
layout(location = 0) in vec3 a_Position;
|
||||
in vec2 a_TexCoord;
|
||||
out vec2 v_TexCoord;
|
||||
void main() {
|
||||
v_TexCoord = a_TexCoord;
|
||||
gl_Position = vec4(a_Position, 1.0);
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String PFB_FRAG = """
|
||||
#version 460 core
|
||||
uniform float frameTimeCounter;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = vec4(frameTimeCounter);
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String CLIP_Z = "gl_Position.z = gl_Position.z * 0.5 + gl_Position.w * 0.5;";
|
||||
|
||||
@BeforeEach
|
||||
void reset() {
|
||||
ShaderManager.clearPrewarmCache();
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearBlocks() {
|
||||
ShaderManager.clearPrewarmCache();
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedSourceReusesTheChainedTransform() {
|
||||
final String expected = ShaderTransformChain.run(FRAG, GL20.GL_FRAGMENT_SHADER);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int a = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
final int b = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
sm.shaderSource(a, FRAG);
|
||||
sm.shaderSource(b, FRAG);
|
||||
|
||||
assertEquals(expected, sm.getShaderSource(a));
|
||||
assertSame(sm.getShaderSource(a), sm.getShaderSource(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void vertexSourceMatchesTheChainAndCarriesClipZ() {
|
||||
final String expected = ShaderTransformChain.run(VERT, GL20.GL_VERTEX_SHADER);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int v = sm.createShader(GL20.GL_VERTEX_SHADER);
|
||||
sm.shaderSource(v, VERT);
|
||||
|
||||
assertEquals(expected, sm.getShaderSource(v));
|
||||
assertTrue(sm.getShaderSource(v).contains(CLIP_Z));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blockChangeMissesTheCache() {
|
||||
GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.FLOAT)));
|
||||
|
||||
final ShaderManager sm = new ShaderManager(null);
|
||||
final int withBlock = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
sm.shaderSource(withBlock, PFB_FRAG);
|
||||
assertTrue(sm.getShaderSource(withBlock).contains("AngelicaPerFrame"));
|
||||
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String expected = ShaderTransformChain.run(PFB_FRAG, GL20.GL_FRAGMENT_SHADER);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
|
||||
final int withoutBlock = sm.createShader(GL20.GL_FRAGMENT_SHADER);
|
||||
sm.shaderSource(withoutBlock, PFB_FRAG);
|
||||
|
||||
assertEquals(expected, sm.getShaderSource(withoutBlock));
|
||||
assertFalse(sm.getShaderSource(withoutBlock).contains("AngelicaPerFrame"));
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
final class ShaderTransformChain {
|
||||
|
||||
private ShaderTransformChain() {}
|
||||
|
||||
static String run(String source, int glShaderType) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : source;
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public final class TesrMaterial {
|
||||
|
||||
private static final ConcurrentHashMap<TesrMaterial, TesrMaterial> INTERNED = new ConcurrentHashMap<>();
|
||||
|
||||
public static final TesrMaterial CURRENT_STATE = intern(new TesrMaterial(false, 0, 0, 0, 0, false, 0, 0, false, false, false, false, false, 0, false, Transparency.OPAQUE, SpecialRender.NONE, null));
|
||||
public static final TesrMaterial CURRENT_STATE = intern(new TesrMaterial(false, 0, 0, 0, 0, false, 0, 0, false, false, false, false, false, 0, false, false, Transparency.OPAQUE, SpecialRender.NONE, null));
|
||||
|
||||
private static TesrMaterial intern(TesrMaterial material) {
|
||||
final TesrMaterial existing = INTERNED.putIfAbsent(material, material);
|
||||
@@ -39,6 +39,7 @@ public final class TesrMaterial {
|
||||
private final boolean isStream;
|
||||
private final float cutoutAlpha;
|
||||
private final boolean isDepthEqual;
|
||||
private final boolean isUnfilteredAtlas;
|
||||
private final Transparency transparency;
|
||||
private final SpecialRender special;
|
||||
private final TesrShader shader;
|
||||
@@ -59,6 +60,7 @@ public final class TesrMaterial {
|
||||
private boolean stream;
|
||||
private float cutoutAlpha;
|
||||
private boolean depthEqual;
|
||||
private boolean unfilteredAtlas;
|
||||
private Transparency transparency = Transparency.OPAQUE;
|
||||
private SpecialRender special = SpecialRender.NONE;
|
||||
private TesrShader shader;
|
||||
@@ -119,6 +121,11 @@ public final class TesrMaterial {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder unfilteredAtlas() {
|
||||
this.unfilteredAtlas = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Standard alpha blending (glass, liquids). SRC_ALPHA, ONE_MINUS_SRC_ALPHA */
|
||||
public Builder translucent() {
|
||||
this.transparency = Transparency.TRANSLUCENT;
|
||||
@@ -158,7 +165,7 @@ public final class TesrMaterial {
|
||||
if (special != SpecialRender.NONE && shader != null) {
|
||||
throw new IllegalStateException("special() and shader() are mutually exclusive");
|
||||
}
|
||||
return intern(new TesrMaterial(hasColor, red, green, blue, alpha, hasLightmap, lightmapX, lightmapY, noCull, unlit, noDepthWrite, depthOnly, stream, cutoutAlpha, depthEqual, transparency, special, shader));
|
||||
return intern(new TesrMaterial(hasColor, red, green, blue, alpha, hasLightmap, lightmapX, lightmapY, noCull, unlit, noDepthWrite, depthOnly, stream, cutoutAlpha, depthEqual, unfilteredAtlas, transparency, special, shader));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/** TESR mesh cache/batch API (Render thread only) */
|
||||
@API(owner = "angelica", provides = "angelica|tesr", apiVersion = "1")
|
||||
@API(owner = "angelica", provides = "angelica|tesr", apiVersion = "2")
|
||||
package com.gtnewhorizons.angelica.api.tesr;
|
||||
|
||||
import cpw.mods.fml.common.API;
|
||||
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.client.font;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
|
||||
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
|
||||
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
@@ -392,13 +393,21 @@ public class BatchingFontRenderer {
|
||||
idxWriterIndex += 6;
|
||||
}
|
||||
|
||||
/** Draw layers; see {@link FontDrawCmd#layer}. */
|
||||
private static final int LAYER_BACKGROUND = -1;
|
||||
private static final int LAYER_DEFAULT = 0;
|
||||
|
||||
private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture, boolean isUnicode) {
|
||||
pushDrawCmd(startIdx, idxCount, texture, isUnicode, LAYER_DEFAULT);
|
||||
}
|
||||
|
||||
private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture, boolean isUnicode, int layer) {
|
||||
arenaOwner = this;
|
||||
// Never coalesce into a command below the watermark - it belongs to a sealed segment.
|
||||
if (batchCommands.size() > batchSealedEnd) {
|
||||
final FontDrawCmd lastCmd = batchCommands.get(batchCommands.size() - 1);
|
||||
final int prevEndVtx = lastCmd.startVtx + lastCmd.idxCount;
|
||||
if (prevEndVtx == startIdx && lastCmd.texture == texture) {
|
||||
if (prevEndVtx == startIdx && lastCmd.texture == texture && lastCmd.layer == layer) {
|
||||
// Coalesce into one
|
||||
lastCmd.idxCount += idxCount;
|
||||
return;
|
||||
@@ -410,7 +419,7 @@ public class BatchingFontRenderer {
|
||||
}
|
||||
}
|
||||
final FontDrawCmd cmd = batchCommandPool.pop();
|
||||
cmd.reset(startIdx, idxCount, texture, isUnicode);
|
||||
cmd.reset(startIdx, idxCount, texture, isUnicode, layer);
|
||||
batchCommands.add(cmd);
|
||||
}
|
||||
|
||||
@@ -420,8 +429,19 @@ public class BatchingFontRenderer {
|
||||
public int idxCount;
|
||||
public boolean isUnicode;
|
||||
public ResourceLocation texture;
|
||||
/**
|
||||
* Sorted ahead of the texture, so lower layers draw first. Sorting by texture
|
||||
* means submission order is not kept, so anything belonging under the glyphs
|
||||
* needs a lower layer rather than an earlier push.
|
||||
*/
|
||||
public int layer;
|
||||
|
||||
public void reset(int startVtx, int vtxCount, ResourceLocation texture, boolean isUnicode) {
|
||||
reset(startVtx, vtxCount, texture, isUnicode, LAYER_DEFAULT);
|
||||
}
|
||||
|
||||
public void reset(int startVtx, int vtxCount, ResourceLocation texture, boolean isUnicode, int layer) {
|
||||
this.layer = layer;
|
||||
this.startVtx = startVtx;
|
||||
this.idxCount = vtxCount;
|
||||
this.texture = texture;
|
||||
@@ -456,9 +476,12 @@ public class BatchingFontRenderer {
|
||||
+ ']';
|
||||
}
|
||||
|
||||
public static final Comparator<FontDrawCmd> DRAW_ORDER_COMPARATOR = Comparator.comparing((FontDrawCmd fdc) -> fdc.texture,
|
||||
Comparator.nullsLast(Comparator.comparing(ResourceLocation::getResourceDomain)
|
||||
.thenComparing(ResourceLocation::getResourcePath))).thenComparing(fdc -> fdc.startVtx);
|
||||
public static final Comparator<FontDrawCmd> DRAW_ORDER_COMPARATOR =
|
||||
Comparator.comparingInt((FontDrawCmd fdc) -> fdc.layer)
|
||||
.thenComparing(fdc -> fdc.texture,
|
||||
Comparator.nullsLast(Comparator.comparing(ResourceLocation::getResourceDomain)
|
||||
.thenComparing(ResourceLocation::getResourcePath)))
|
||||
.thenComparing(fdc -> fdc.startVtx);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1281,6 +1304,7 @@ public class BatchingFontRenderer {
|
||||
boolean curWave = false;
|
||||
boolean curDinnerbone = false;
|
||||
boolean curGradient = false;
|
||||
long curCustomEffects = 0L;
|
||||
boolean curShadow = false;
|
||||
boolean curShadowCustomColor = false;
|
||||
int curShadowColorOverride = 0;
|
||||
@@ -1331,6 +1355,16 @@ public class BatchingFontRenderer {
|
||||
if (rgb != -1) {
|
||||
curRainbow = false;
|
||||
curGradient = false;
|
||||
curCustomEffects = 0L;
|
||||
// Styles normally carry through a hex colour; a text mod that owns
|
||||
// this grammar can say otherwise, and the widths follow the same call.
|
||||
if (FontRendering.hexColorResetsStyles()) {
|
||||
curRandom = false;
|
||||
curBold = false;
|
||||
curStrikethrough = false;
|
||||
curUnderline = false;
|
||||
curItalic = false;
|
||||
}
|
||||
curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF);
|
||||
curShadowColor = (curShadowColor & 0xFF000000) | ((rgb & 0xFCFCFC) >> 2);
|
||||
charIdx += SECTION_X_PAYLOAD;
|
||||
@@ -1346,6 +1380,7 @@ public class BatchingFontRenderer {
|
||||
curItalic = false;
|
||||
curRainbow = false;
|
||||
curGradient = false;
|
||||
curCustomEffects = 0L;
|
||||
// wave/dinnerbone NOT reset — they're positional effects, independent of color
|
||||
|
||||
final int colorIdx = is09 ? (fmtCode - '0') : (fmtCode - 'a' + 10);
|
||||
@@ -1370,6 +1405,7 @@ public class BatchingFontRenderer {
|
||||
} else if (fmtCode == 'q' && AngelicaConfig.enableRainbow) {
|
||||
curRainbow = true;
|
||||
curGradient = false;
|
||||
curCustomEffects = 0L;
|
||||
rainbowCharIndex = 0;
|
||||
} else if (fmtCode == 'z' && AngelicaConfig.enableWaveText) {
|
||||
curWave = !curWave;
|
||||
@@ -1394,6 +1430,7 @@ public class BatchingFontRenderer {
|
||||
if (color1 != -1 && color2 != -1) {
|
||||
curGradient = true;
|
||||
curRainbow = false;
|
||||
curCustomEffects = 0L;
|
||||
gradientStartRgb = color1;
|
||||
gradientEndRgb = color2;
|
||||
gradientCharIndex = 0;
|
||||
@@ -1401,6 +1438,10 @@ public class BatchingFontRenderer {
|
||||
gradientStep = gradientTotalChars > 1 ? 1f / (gradientTotalChars - 1) : 0f;
|
||||
charIdx += GRADIENT_PAYLOAD;
|
||||
}
|
||||
} else if (FontEffectRegistry.isRegistered(fmtCode)) {
|
||||
// Toggles, like wave and dinnerbone, so a span can be closed
|
||||
// without a colour or §r. Those still clear it as before.
|
||||
curCustomEffects ^= FontEffectRegistry.bit(fmtCode);
|
||||
} else if (fmtCode == 'r') {
|
||||
curRandom = false;
|
||||
curBold = false;
|
||||
@@ -1411,6 +1452,7 @@ public class BatchingFontRenderer {
|
||||
curWave = false;
|
||||
curDinnerbone = false;
|
||||
curGradient = false;
|
||||
curCustomEffects = 0L;
|
||||
curShadow = false;
|
||||
curShadowCustomColor = false;
|
||||
curColor = color;
|
||||
@@ -1434,19 +1476,23 @@ public class BatchingFontRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
if (curRandom) {
|
||||
chr = FontProviderMC.get(this.isSGA).getRandomReplacement(chr);
|
||||
}
|
||||
// ASCII space, NBSP, NNBSP, decided before obfuscation as vanilla does.
|
||||
final boolean whitespace = chr == ' ' || chr == '\u00A0' || chr == '\u202F';
|
||||
|
||||
FontProvider fontProvider = FontStrategist.getFontProvider(this, chr, FontConfig.enableCustomFont, unicodeFlag);
|
||||
|
||||
// Asked of the provider that draws the glyph, so §k keeps to the font in use
|
||||
// and to the width it was measured at, under a custom or unicode font too.
|
||||
if (curRandom && !whitespace) {
|
||||
chr = fontProvider.getRandomReplacement(chr);
|
||||
}
|
||||
|
||||
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * fontProvider.getYScaleMultiplier() / 2);
|
||||
float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY * fontProvider.getYScaleMultiplier();
|
||||
|
||||
visibleCharIndex++;
|
||||
|
||||
// Check ASCII space, NBSP, NNBSP
|
||||
if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') {
|
||||
if (whitespace) {
|
||||
curX += 4 * this.getWhitespaceScale() + (curBold ? 1 : 0);
|
||||
continue;
|
||||
}
|
||||
@@ -1479,7 +1525,6 @@ public class BatchingFontRenderer {
|
||||
final int shadowCopies = FontConfig.shadowCopies;
|
||||
final int boldCopies = FontConfig.boldCopies;
|
||||
final ResourceLocation texture = fontProvider.getTexture(chr);
|
||||
final int idxId = idxWriterIndex;
|
||||
|
||||
// Wave: Y offset via sine wave
|
||||
float renderY = heightNorth;
|
||||
@@ -1488,27 +1533,55 @@ public class BatchingFontRenderer {
|
||||
renderY += (float) Math.sin(visibleCharIndex * WAVE_FREQUENCY + time) * AngelicaConfig.waveAmplitude;
|
||||
}
|
||||
|
||||
float renderX = curX;
|
||||
int glyphColor = curColor;
|
||||
int glyphShadowColor = curShadowColor;
|
||||
int glyphBackground = 0;
|
||||
if (curCustomEffects != 0L) {
|
||||
long fx = curCustomEffects;
|
||||
while (fx != 0L) {
|
||||
final CustomGlyphEffect effect = FontEffectRegistry.get(Long.numberOfTrailingZeros(fx));
|
||||
fx &= fx - 1L;
|
||||
glyphColor = effect.transformColor(glyphColor, false, visibleCharIndex);
|
||||
glyphShadowColor = effect.transformColor(glyphShadowColor, true, visibleCharIndex);
|
||||
renderX += effect.offsetX(visibleCharIndex);
|
||||
renderY += effect.offsetY(visibleCharIndex);
|
||||
final int background = effect.backgroundColor(visibleCharIndex);
|
||||
if (background != 0) glyphBackground = background;
|
||||
}
|
||||
}
|
||||
|
||||
// Untextured, with its own draw command, like the underline below.
|
||||
if (glyphBackground != 0) {
|
||||
final int bgIdx = idxWriterIndex;
|
||||
pushUntexRect(renderX, renderY, glyphW, heightSouth, glyphBackground);
|
||||
pushDrawCmd(bgIdx, 6, null, false, LAYER_BACKGROUND);
|
||||
}
|
||||
|
||||
// After the background, so this command covers only what is counted below.
|
||||
final int idxId = idxWriterIndex;
|
||||
|
||||
final boolean drawShadow = enableShadow || curShadow;
|
||||
if (drawShadow) {
|
||||
final int effectiveShadowColor = curShadowCustomColor
|
||||
? ((curColor & 0xFF000000) | (curShadowColorOverride & 0x00FFFFFF))
|
||||
: curShadowColor;
|
||||
? ((glyphColor & 0xFF000000) | (curShadowColorOverride & 0x00FFFFFF))
|
||||
: glyphShadowColor;
|
||||
for (int n = 1; n <= shadowCopies; n++) {
|
||||
final float shadowOffsetPart = shadowOffset * ((float) n / shadowCopies);
|
||||
pushTexRect(curX + shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, effectiveShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
pushTexRect(renderX + shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, effectiveShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
|
||||
if (curBold) {
|
||||
pushTexRect(curX + 2.0f * shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, effectiveShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
pushTexRect(renderX + 2.0f * shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, effectiveShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pushTexRect(curX, renderY, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
pushTexRect(renderX, renderY, glyphW - 1.0f, heightSouth, itOff, glyphColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
|
||||
if (curBold) {
|
||||
for (int n = 1; n <= boldCopies; n++) {
|
||||
final float shadowOffsetPart = shadowOffset * ((float) n / boldCopies);
|
||||
pushTexRect(curX + shadowOffsetPart, renderY, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
pushTexRect(renderX + shadowOffsetPart, renderY, glyphW - 1.0f, heightSouth, itOff, glyphColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1530,15 +1603,18 @@ public class BatchingFontRenderer {
|
||||
underlineEndX = curX;
|
||||
strikethroughEndX = curX;
|
||||
|
||||
if ((curRainbow || curGradient) && curUnderline && underlineStartX != underlineEndX) {
|
||||
// Flushed per glyph while the colour moves, so the line under the text is the
|
||||
// colour of the glyph above it rather than one flat run.
|
||||
final boolean perGlyphColor = curRainbow || curGradient || curCustomEffects != 0L;
|
||||
if (perGlyphColor && curUnderline && underlineStartX != underlineEndX) {
|
||||
final int ulIdx = idxWriterIndex;
|
||||
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor);
|
||||
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, glyphColor);
|
||||
pushDrawCmd(ulIdx, 6, null, false);
|
||||
underlineStartX = underlineEndX;
|
||||
}
|
||||
if ((curRainbow || curGradient) && curStrikethrough && strikethroughStartX != strikethroughEndX) {
|
||||
if (perGlyphColor && curStrikethrough && strikethroughStartX != strikethroughEndX) {
|
||||
final int stIdx = idxWriterIndex;
|
||||
pushUntexRect(strikethroughStartX, strikethroughY, strikethroughEndX - strikethroughStartX, glyphScaleY, curColor);
|
||||
pushUntexRect(strikethroughStartX, strikethroughY, strikethroughEndX - strikethroughStartX, glyphScaleY, glyphColor);
|
||||
pushDrawCmd(stIdx, 6, null, false);
|
||||
strikethroughStartX = strikethroughEndX;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
/**
|
||||
* Utility for converting {@code &}-based shorthand into {@code §}-based
|
||||
@@ -17,6 +21,8 @@ public final class ColorCodeUtils {
|
||||
|
||||
private ColorCodeUtils() {}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("AngelicaFontEffects");
|
||||
|
||||
public static final char FORMATTING_CHAR = '§';
|
||||
public static final char ESCAPED_AMPERSAND = '';
|
||||
|
||||
@@ -69,8 +75,26 @@ public final class ColorCodeUtils {
|
||||
private static String lastConversionInput;
|
||||
private static String lastConversionOutput;
|
||||
|
||||
private static BooleanSupplier conversionSuppressor;
|
||||
|
||||
/**
|
||||
* Lets text mods that draw format codes as literal characters (e.g. inside edit GUIs)
|
||||
* suspend {@code &} conversion for strings rendered while the supplier returns true.
|
||||
*/
|
||||
public static void setConversionSuppressor(BooleanSupplier suppressor) {
|
||||
if (suppressor != null && conversionSuppressor != null && suppressor != conversionSuppressor) {
|
||||
LOGGER.info("Replacing the & conversion suppressor; only the newest one is consulted");
|
||||
}
|
||||
conversionSuppressor = suppressor;
|
||||
}
|
||||
|
||||
public static boolean isConversionSuppressed() {
|
||||
final BooleanSupplier suppressor = conversionSuppressor;
|
||||
return suppressor != null && suppressor.getAsBoolean();
|
||||
}
|
||||
|
||||
public static String convertAmpersandToSectionX(String text) {
|
||||
if (text == null || !AngelicaConfig.enableAmpersandConversion) return text;
|
||||
if (text == null || !AngelicaConfig.enableAmpersandConversion || isConversionSuppressed()) return text;
|
||||
|
||||
if (text == lastConversionInput) return lastConversionOutput;
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
/**
|
||||
* A per-glyph text effect another mod can attach to an unused {@code §} format code through
|
||||
* {@link FontEffectRegistry}. While the code is active the batched font renderer invokes the
|
||||
* effect for every visible glyph it emits.
|
||||
* <p>
|
||||
* The format code toggles the effect, and any colour code ({@code §0}-{@code §f}, {@code §x},
|
||||
* {@code §g}, {@code §q}) or {@code §r} clears it, as with the renderer's own colour-bound
|
||||
* effects. Wave and dinnerbone are positional and leave it alone.
|
||||
*/
|
||||
public interface CustomGlyphEffect {
|
||||
|
||||
/**
|
||||
* Transforms the colour a glyph is about to be drawn with. Called once for the main pass and
|
||||
* once for the shadow pass (receiving the already-darkened shadow colour). Implementations
|
||||
* should preserve the alpha channel of the input.
|
||||
*/
|
||||
int transformColor(int argb, boolean shadowPass, int glyphIndex);
|
||||
|
||||
/**
|
||||
* Quad drawn behind the glyph as {@code 0xAARRGGBB}, or {@code 0} for none. Lets an
|
||||
* effect mark a span without taking over the text colour. Where two active effects
|
||||
* both draw one, the later format-code letter wins.
|
||||
*/
|
||||
default int backgroundColor(int glyphIndex) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Horizontal offset added to the glyph quad, in text-space pixels. */
|
||||
default float offsetX(int glyphIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
/** Vertical offset added to the glyph quad, in text-space pixels. */
|
||||
default float offsetY(int glyphIndex) {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Maps unused {@code §} format-code letters to {@link CustomGlyphEffect}s supplied by other mods
|
||||
* (e.g. HexText's ignite and shake). Letters already used by vanilla formatting or the batched
|
||||
* font renderer's own grammar cannot be claimed.
|
||||
*/
|
||||
public final class FontEffectRegistry {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("AngelicaFontEffects");
|
||||
|
||||
/** Letters with a meaning in vanilla formatting or the batched renderer's grammar. */
|
||||
private static final String RESERVED_CODES = "abcdefgklmnoqruvxz";
|
||||
|
||||
private static final CustomGlyphEffect[] EFFECTS = new CustomGlyphEffect[26];
|
||||
private static volatile boolean anyRegistered = false;
|
||||
|
||||
private FontEffectRegistry() {}
|
||||
|
||||
/**
|
||||
* Claims {@code code} (a-z, case-insensitive) for the given effect.
|
||||
*
|
||||
* @return false when the letter is reserved, already claimed, or invalid
|
||||
*/
|
||||
public static synchronized boolean register(char code, CustomGlyphEffect effect) {
|
||||
final char lower = Character.toLowerCase(code);
|
||||
if (lower < 'a' || lower > 'z' || RESERVED_CODES.indexOf(lower) != -1 || effect == null) {
|
||||
LOGGER.warn("Rejected font effect registration for reserved or invalid code '{}'", code);
|
||||
return false;
|
||||
}
|
||||
if (EFFECTS[lower - 'a'] != null) {
|
||||
LOGGER.warn("Rejected font effect registration for already-claimed code '{}'", code);
|
||||
return false;
|
||||
}
|
||||
EFFECTS[lower - 'a'] = effect;
|
||||
anyRegistered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean hasAny() {
|
||||
return anyRegistered;
|
||||
}
|
||||
|
||||
public static boolean isRegistered(char lowerCode) {
|
||||
return anyRegistered && lowerCode >= 'a' && lowerCode <= 'z' && EFFECTS[lowerCode - 'a'] != null;
|
||||
}
|
||||
|
||||
static long bit(char lowerCode) {
|
||||
return 1L << (lowerCode - 'a');
|
||||
}
|
||||
|
||||
static CustomGlyphEffect get(int bitIndex) {
|
||||
return EFFECTS[bitIndex];
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,12 @@ import net.minecraft.util.ResourceLocation;
|
||||
|
||||
public interface FontProvider {
|
||||
|
||||
/** Tries before {@link #getRandomReplacement} gives up and leaves the character alone. */
|
||||
int RANDOM_GLYPH_TRIES = 64;
|
||||
|
||||
/**
|
||||
* For use with §k. Should fetch a character of the same width as provided.
|
||||
* For use with §k. Should fetch a character of the same width as provided, and one this
|
||||
* provider can draw, since the provider is picked for the original character.
|
||||
*/
|
||||
char getRandomReplacement(char chr);
|
||||
boolean isGlyphAvailable(char chr);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import jss.util.RandomXoshiro256StarStar;
|
||||
import lombok.Setter;
|
||||
import lombok.Value;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -31,6 +32,7 @@ public final class FontProviderCustom implements FontProvider {
|
||||
static final int ATLAS_SIZE = 128;
|
||||
static final int ATLAS_COUNT = 512;
|
||||
private final byte id; // 0 - primary font, 1 - fallback font
|
||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||
private FontAtlas[] fontAtlases = new FontAtlas[ATLAS_COUNT];
|
||||
private float currentFontQuality = FontConfig.customFontQuality;
|
||||
@Setter
|
||||
@@ -239,8 +241,29 @@ public final class FontProviderCustom implements FontProvider {
|
||||
return (getAtlas(chr).glyphData[chr % ATLAS_SIZE] != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A random glyph of the same width, for {@code §k}. Kept inside the character's own
|
||||
* atlas: atlases are rasterized on first use, so reaching outside one would build a
|
||||
* new atlas mid-frame. Slots the font has no glyph for are skipped.
|
||||
*/
|
||||
@Override
|
||||
public char getRandomReplacement(char chr) {
|
||||
if (this.font == null) {
|
||||
return chr;
|
||||
}
|
||||
final FontAtlas atlas = getAtlas(chr);
|
||||
final GlyphData target = atlas.glyphData[chr % ATLAS_SIZE];
|
||||
if (target == null) {
|
||||
return chr;
|
||||
}
|
||||
final int atlasStart = (chr / ATLAS_SIZE) * ATLAS_SIZE;
|
||||
for (int attempt = 0; attempt < RANDOM_GLYPH_TRIES; attempt++) {
|
||||
final int slot = fontRandom.nextInt(ATLAS_SIZE);
|
||||
final GlyphData candidate = atlas.glyphData[slot];
|
||||
if (candidate != null && candidate.xAdvance == target.xAdvance) {
|
||||
return (char) (atlasStart + slot);
|
||||
}
|
||||
}
|
||||
return chr;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,16 +43,27 @@ public final class FontProviderMC implements FontProvider {
|
||||
return MCFONT_ASCII_MAP.containsKey(chr);
|
||||
}
|
||||
|
||||
/**
|
||||
* A random glyph of the same width, for {@code §k}. Only slots the reverse lookup
|
||||
* agrees with are eligible: {@link #MCFONT_CHARS} repeats {@code U+0000} and the map
|
||||
* keeps the last, so other slots would measure from a different one. Bounded, since
|
||||
* a resource pack may ship widths where nothing matches.
|
||||
*/
|
||||
public char getRandomReplacement(char chr){
|
||||
int lutIndex = lookupMcFontPosition(chr);
|
||||
if (lutIndex != -1) {
|
||||
int randomReplacementIndex;
|
||||
do {
|
||||
randomReplacementIndex = fontRandom.nextInt(this.charWidth.length);
|
||||
} while (this.charWidth[lutIndex] != this.charWidth[randomReplacementIndex]);
|
||||
|
||||
lutIndex = randomReplacementIndex;
|
||||
return MCFONT_CHARS.charAt(lutIndex);
|
||||
final int lutIndex = lookupMcFontPosition(chr);
|
||||
if (lutIndex == -1 || this.charWidth == null || lutIndex >= this.charWidth.length) {
|
||||
return chr;
|
||||
}
|
||||
final int targetWidth = this.charWidth[lutIndex];
|
||||
for (int attempt = 0; attempt < RANDOM_GLYPH_TRIES; attempt++) {
|
||||
final int candidate = fontRandom.nextInt(this.charWidth.length);
|
||||
if (this.charWidth[candidate] != targetWidth) {
|
||||
continue;
|
||||
}
|
||||
final char replacement = MCFONT_CHARS.charAt(candidate);
|
||||
if (MCFONT_ASCII_MAP.get(replacement) == candidate) {
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
return chr;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import jss.util.RandomXoshiro256StarStar;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.IReloadableResourceManager;
|
||||
import net.minecraft.client.resources.IResourceManager;
|
||||
@@ -34,6 +35,7 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
|
||||
|
||||
private static final ResourceLocation[] unicodePageLocations = new ResourceLocation[256];
|
||||
private final byte[] glyphWidth = new byte[65536];
|
||||
private final RandomXoshiro256StarStar fontRandom = new RandomXoshiro256StarStar();
|
||||
|
||||
private ResourceLocation getUnicodePageLocation(int page) {
|
||||
final ResourceLocation lookup = unicodePageLocations[page];
|
||||
@@ -53,8 +55,24 @@ public final class FontProviderUnicode implements FontProvider, IResourceManager
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A random glyph of the same width, for {@code §k}. Kept inside the character's own
|
||||
* 256-glyph page so the swap neither pulls in another page's texture nor splits the
|
||||
* batch. Empty entries are left alone, and never swapped in for a glyph that draws.
|
||||
*/
|
||||
@Override
|
||||
public char getRandomReplacement(char chr) {
|
||||
if (this.glyphWidth[chr] == 0) {
|
||||
return chr;
|
||||
}
|
||||
final int pageStart = (chr / 256) * 256;
|
||||
final float targetWidth = getXAdvance(chr);
|
||||
for (int attempt = 0; attempt < RANDOM_GLYPH_TRIES; attempt++) {
|
||||
final char candidate = (char) (pageStart + fontRandom.nextInt(256));
|
||||
if (this.glyphWidth[candidate] != 0 && getXAdvance(candidate) == targetWidth) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return chr;
|
||||
}
|
||||
|
||||
|
||||
+3
-13
@@ -5,7 +5,6 @@ import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.ITessellatorInstance;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
@@ -28,8 +27,7 @@ class DeferredBatchTessellator extends DirectTessellator {
|
||||
record DrawRange(long stateKey, int byteOffset, int byteLength, int vertexCount, int drawMode, int flags) {}
|
||||
|
||||
private final List<DrawRange> ranges = new ArrayList<>();
|
||||
private final Matrix4f defaultModelview = new Matrix4f();
|
||||
private final Matrix4f defaultModelviewInverse = new Matrix4f();
|
||||
private final ModelViewDelta modelViewDelta = new ModelViewDelta();
|
||||
private final Matrix4f deltaMatrix = new Matrix4f(); // reusable scratch
|
||||
private final Vector3f scratch = new Vector3f();
|
||||
private Matrix4fc currentTransform;
|
||||
@@ -68,22 +66,14 @@ class DeferredBatchTessellator extends DirectTessellator {
|
||||
|
||||
/** Snapshot the current modelview as the "default" for this bracket. */
|
||||
void snapshotDefaultModelview() {
|
||||
defaultModelview.set(GLStateManager.getModelViewMatrix());
|
||||
defaultModelview.invert(defaultModelviewInverse);
|
||||
modelViewDelta.snapshot();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int interceptDraw(Tessellator tess) {
|
||||
inIntercept = true;
|
||||
try {
|
||||
// Compute delta: if current MV differs from default, pre-transform vertices
|
||||
final Matrix4f currentMV = GLStateManager.getModelViewMatrix();
|
||||
if (!currentMV.equals(defaultModelview)) {
|
||||
defaultModelviewInverse.mul(currentMV, deltaMatrix);
|
||||
this.currentTransform = deltaMatrix;
|
||||
} else {
|
||||
this.currentTransform = null;
|
||||
}
|
||||
this.currentTransform = modelViewDelta.deltaOrNull(deltaMatrix);
|
||||
|
||||
final int byteOffsetBefore = bufferLimit();
|
||||
final int vertCount = tess.vertexCount;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.gtnewhorizons.angelica.client.rendering;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
|
||||
public final class ModelViewDelta {
|
||||
|
||||
private final Matrix4f base = new Matrix4f();
|
||||
private final Matrix4f baseInverse = new Matrix4f();
|
||||
|
||||
public void snapshot() {
|
||||
base.set(GLStateManager.getModelViewMatrix());
|
||||
base.invert(baseInverse);
|
||||
}
|
||||
|
||||
public Matrix4fc deltaOrNull(Matrix4f scratch) {
|
||||
final Matrix4f current = GLStateManager.getModelViewMatrix();
|
||||
if (current.equals(base)) return null;
|
||||
return baseInverse.mul(current, scratch);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class GTNHLibCompat {
|
||||
|
||||
@Override
|
||||
public boolean handlesAmpCodes() {
|
||||
return AngelicaConfig.enableAmpersandConversion;
|
||||
return AngelicaConfig.enableAmpersandConversion && !ColorCodeUtils.isConversionSuppressed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import net.coderbot.batchedentityrendering.impl.BatchVertexFormats;
|
||||
import net.coderbot.batchedentityrendering.impl.BlendingStateHolder;
|
||||
import net.coderbot.batchedentityrendering.impl.TransparencyType;
|
||||
import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition;
|
||||
import com.gtnewhorizons.angelica.shadercompat.ShaderGlint;
|
||||
import net.coderbot.iris.layer.GbufferPrograms;
|
||||
import net.coderbot.iris.layer.PassOverride;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
@@ -69,9 +70,9 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
|
||||
return TRANSLUCENT;
|
||||
}
|
||||
|
||||
private static MultiPhaseParameters.Builder tesrMaterialPhases(ResourceLocation texture, TesrMaterial material, PassOverride pass, float offsetFactor, float offsetUnits) {
|
||||
private static MultiPhaseParameters.Builder tesrMaterialPhases(ResourceLocation texture, TesrMaterial material, PassOverride pass, float offsetFactor, float offsetUnits, int glintSlot) {
|
||||
final MultiPhaseParameters.Builder b = MultiPhaseParameters.builder();
|
||||
b.texture(texture != null ? new RenderPhase.Texture(texture, false, false) : NO_TEXTURE);
|
||||
b.texture(texture != null ? new RenderPhase.Texture(texture, false, false, material.isUnfilteredAtlas()) : NO_TEXTURE);
|
||||
if (material.isNoCull()) {
|
||||
b.cull(DISABLE_CULLING);
|
||||
}
|
||||
@@ -95,7 +96,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
|
||||
}
|
||||
boolean shaderPhaseTaken = true;
|
||||
switch (material.special()) {
|
||||
case GLINT -> b.shader(SPECIAL_GLINT);
|
||||
case GLINT -> b.shader(glintShader(glintSlot));
|
||||
case BEACON_BEAM -> b.shader(SPECIAL_BEACON_BEAM);
|
||||
case NONE -> {
|
||||
final TesrShader shader = material.shader();
|
||||
@@ -113,19 +114,35 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
|
||||
return b;
|
||||
}
|
||||
|
||||
private static final RenderPhase.Shader SPECIAL_GLINT = new RenderPhase.Shader("angelica_special_glint",
|
||||
() -> GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.GLINT),
|
||||
GbufferPrograms::teardownSpecialRenderCondition);
|
||||
private static final RenderPhase.Shader[] GLINT_SHADERS = new RenderPhase.Shader[ShaderGlint.TINT_SLOTS + 1];
|
||||
|
||||
private static RenderPhase.Shader glintShader(int slot) {
|
||||
RenderPhase.Shader shader = GLINT_SHADERS[slot + 1];
|
||||
if (shader == null) {
|
||||
shader = new RenderPhase.Shader("angelica_special_glint_" + slot,
|
||||
() -> {
|
||||
GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.GLINT);
|
||||
ShaderGlint.bindTintedGlint(slot);
|
||||
},
|
||||
GbufferPrograms::teardownSpecialRenderCondition);
|
||||
GLINT_SHADERS[slot + 1] = shader;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
private static final RenderPhase.Shader SPECIAL_BEACON_BEAM = new RenderPhase.Shader("angelica_special_beacon_beam",
|
||||
() -> GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.BEACON_BEAM),
|
||||
GbufferPrograms::teardownSpecialRenderCondition);
|
||||
|
||||
public static RenderLayer tesr(ResourceLocation texture, TesrMaterial material) {
|
||||
return tesr(texture, material, PassOverride.NONE, 0.0f, 0.0f);
|
||||
return tesr(texture, material, PassOverride.NONE, 0.0f, 0.0f, ShaderGlint.NO_TINT);
|
||||
}
|
||||
|
||||
public static RenderLayer tesr(ResourceLocation texture, TesrMaterial material, PassOverride pass, float offsetFactor, float offsetUnits) {
|
||||
final MultiPhaseParameters.Builder b = tesrMaterialPhases(texture, material, pass, offsetFactor, offsetUnits).shadeModel(SMOOTH_SHADE_MODEL);
|
||||
return tesr(texture, material, pass, offsetFactor, offsetUnits, ShaderGlint.NO_TINT);
|
||||
}
|
||||
|
||||
public static RenderLayer tesr(ResourceLocation texture, TesrMaterial material, PassOverride pass, float offsetFactor, float offsetUnits, int glintSlot) {
|
||||
final MultiPhaseParameters.Builder b = tesrMaterialPhases(texture, material, pass, offsetFactor, offsetUnits, glintSlot).shadeModel(SMOOTH_SHADE_MODEL);
|
||||
final boolean sortsTranslucent = material.transparency() != TesrMaterial.Transparency.OPAQUE;
|
||||
return of("angelica_tesr_" + material.transparency().name().toLowerCase(Locale.ROOT) + pass.nameSuffix()
|
||||
+ (offsetFactor == 0.0f && offsetUnits == 0.0f ? "" : "_offset" + offsetFactor + "_" + offsetUnits),
|
||||
@@ -135,7 +152,7 @@ public abstract class RenderLayer extends RenderPhase { // Aka: RenderType (Iris
|
||||
public static RenderLayer tesrNoPass(ResourceLocation texture, TesrMaterial material) {
|
||||
return of("angelica_tesr_nopass_" + material.transparency().name().toLowerCase(Locale.ROOT),
|
||||
BatchVertexFormats.POSITION_COLOR_TEXTURE_LIGHTF_NORMAL, GL11.GL_QUADS, 65536, true, false,
|
||||
tesrMaterialPhases(texture, material, PassOverride.NONE, 0.0f, 0.0f).build(false));
|
||||
tesrMaterialPhases(texture, material, PassOverride.NONE, 0.0f, 0.0f, ShaderGlint.NO_TINT).build(false));
|
||||
}
|
||||
|
||||
public static RenderLayer getOutline(ResourceLocation texture, RenderPhase.Cull cull) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import net.coderbot.batchedentityrendering.impl.TransparencyType;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.TextureManager;
|
||||
import net.minecraft.client.renderer.texture.TextureMap;
|
||||
import net.minecraft.client.renderer.texture.TextureUtil;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
@@ -406,18 +407,30 @@ public abstract class RenderPhase {
|
||||
private final Optional<ResourceLocation> id;
|
||||
protected boolean bilinear;
|
||||
protected boolean mipmap;
|
||||
protected boolean unfilteredAtlas;
|
||||
|
||||
public Texture(ResourceLocation id, boolean bilinear, boolean mipmap) {
|
||||
this(id, bilinear, mipmap, false);
|
||||
}
|
||||
|
||||
public Texture(ResourceLocation id, boolean bilinear, boolean mipmap, boolean unfilteredAtlas) {
|
||||
super("texture", () -> {
|
||||
GLStateManager.enableTexture();
|
||||
TextureManager lv = Minecraft.getMinecraft().getTextureManager();
|
||||
lv.bindTexture(id);
|
||||
//GLStateManager.setFilter(bilinear, mipmap); // breaks textures. TODO find out why
|
||||
if (unfilteredAtlas) {
|
||||
TextureUtil.func_152777_a(false, false, 1.0F);
|
||||
}
|
||||
}, () -> {
|
||||
if (unfilteredAtlas) {
|
||||
TextureUtil.func_147945_b();
|
||||
}
|
||||
});
|
||||
this.id = Optional.of(id);
|
||||
this.bilinear = bilinear;
|
||||
this.mipmap = mipmap;
|
||||
this.unfilteredAtlas = unfilteredAtlas;
|
||||
}
|
||||
|
||||
public Texture() {
|
||||
@@ -425,6 +438,7 @@ public abstract class RenderPhase {
|
||||
this.id = Optional.empty();
|
||||
this.bilinear = false;
|
||||
this.mipmap = false;
|
||||
this.unfilteredAtlas = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -433,7 +447,7 @@ public abstract class RenderPhase {
|
||||
return true;
|
||||
} else if (object != null && this.getClass() == object.getClass()) {
|
||||
Texture lv = (Texture)object;
|
||||
return this.id.equals(lv.id) && this.bilinear == lv.bilinear && this.mipmap == lv.mipmap;
|
||||
return this.id.equals(lv.id) && this.bilinear == lv.bilinear && this.mipmap == lv.mipmap && this.unfilteredAtlas == lv.unfilteredAtlas;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -446,7 +460,7 @@ public abstract class RenderPhase {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name + '[' + this.id + "(blur=" + this.bilinear + ", mipmap=" + this.mipmap + ")]";
|
||||
return this.name + '[' + this.id + "(blur=" + this.bilinear + ", mipmap=" + this.mipmap + ", unfilteredAtlas=" + this.unfilteredAtlas + ")]";
|
||||
}
|
||||
|
||||
protected Optional<ResourceLocation> getId() {
|
||||
|
||||
@@ -127,11 +127,16 @@ public class AngelicaConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableTESRChestCache;
|
||||
|
||||
@Config.Comment("Batch and instance living-entity model parts on FFP-managed passes")
|
||||
@Config.Comment("Batch and instance entity model parts, items, and shadows on FFP-managed passes")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableEntityBatching;
|
||||
|
||||
@Config.Comment("Draw cuboid model parts from a shared unit cube (requires entity batching)")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableCubeInstancing;
|
||||
|
||||
@Config.Comment("Skip the end-of-frame shader buffer copy by ping-ponging buffers. Disable if a shader pack misrenders.")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
@@ -251,7 +256,7 @@ public class AngelicaConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean optimizeInWorldItemRendering;
|
||||
|
||||
@Config.Comment("Upper limit for the amount of VBO's to cache for optimized item rendering. Higher number can potentially use more VRAM.")
|
||||
@Config.Comment("Upper limit for the amount of cached item meshes (VBOs and batched item templates) for optimized item rendering. Higher number can potentially use more memory and VRAM.")
|
||||
@Config.DefaultInt(512)
|
||||
@Config.RangeInt(min = 256, max = 1024)
|
||||
public static int itemRendererCacheSize;
|
||||
@@ -463,6 +468,10 @@ public class AngelicaConfig {
|
||||
GpuCulling.setMode(gpuCullingMode == null ? GpuCullingMode.CPU_ONLY : gpuCullingMode);
|
||||
}
|
||||
|
||||
public static boolean cubeInstancingEnabled() {
|
||||
return enableEntityBatching && enableCubeInstancing;
|
||||
}
|
||||
|
||||
public static GLProfile getEffectiveGlProfile() {
|
||||
final String sys = SystemProperties.GL_PROFILE.trim().toUpperCase();
|
||||
if ("GLES".equals(sys)) return GLProfile.ES;
|
||||
|
||||
@@ -15,9 +15,11 @@ final class FlybyCommandSender implements ICommandSender {
|
||||
private static final String NAME = "Flyby";
|
||||
|
||||
private final EntityPlayerMP player;
|
||||
private final ChunkCoordinates anchor;
|
||||
|
||||
FlybyCommandSender(EntityPlayerMP player) {
|
||||
FlybyCommandSender(EntityPlayerMP player, ChunkCoordinates anchor) {
|
||||
this.player = player;
|
||||
this.anchor = anchor;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,7 +44,7 @@ final class FlybyCommandSender implements ICommandSender {
|
||||
|
||||
@Override
|
||||
public ChunkCoordinates getPlayerCoordinates() {
|
||||
return this.player.getPlayerCoordinates();
|
||||
return this.anchor;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.gtnewhorizons.angelica.debug.flyby;
|
||||
|
||||
record FlybyOrigin(double x, double z, float yaw) {
|
||||
|
||||
static FlybyOrigin parse(String raw) {
|
||||
if (raw == null || raw.isBlank()) return null;
|
||||
final String[] parts = raw.split(",", -1);
|
||||
if (parts.length != 2 && parts.length != 3) throw new IllegalArgumentException("Expected x,z[,yaw] but got '" + raw + "'");
|
||||
try {
|
||||
return new FlybyOrigin(Double.parseDouble(parts[0].trim()), Double.parseDouble(parts[1].trim()), parts.length == 3 ? Float.parseFloat(parts[2].trim()) : 0.0F);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Expected x,z[,yaw] but got '" + raw + "'", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,19 +15,23 @@ import net.minecraft.client.gui.GuiMainMenu;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.util.ChatComponentText;
|
||||
import net.minecraft.util.ChunkCoordinates;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.world.MinecraftException;
|
||||
import net.minecraft.world.WorldServer;
|
||||
import net.minecraft.world.chunk.Chunk;
|
||||
import net.minecraft.world.chunk.IChunkProvider;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.opengl.Display;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/** Flies a deterministic camera path */
|
||||
public final class FlybyRunner {
|
||||
public static final FlybyRunner INSTANCE = new FlybyRunner();
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica/Flyby");
|
||||
private static final long[] NO_FRAMES = new long[0];
|
||||
private static final double[] NO_PATH = new double[0];
|
||||
@@ -36,7 +40,9 @@ public final class FlybyRunner {
|
||||
private static final String[] NO_COMMANDS = new String[0];
|
||||
private static final int MAX_RECORDED_FRAMES = 200_000;
|
||||
|
||||
private enum State { IDLE, WAITING, WARMUP, RUNNING, SETTLE, EXITING, DONE }
|
||||
public static final FlybyRunner INSTANCE = new FlybyRunner();
|
||||
|
||||
private enum State { IDLE, WAITING, PREPARING, WARMUP, RUNNING, SETTLE, EXITING, DONE }
|
||||
|
||||
private static final int EXIT_TICKS = 20;
|
||||
private static final int SETTLE_TICKS = 40;
|
||||
@@ -52,10 +58,9 @@ public final class FlybyRunner {
|
||||
private boolean waitForFocus;
|
||||
private boolean exitWhenDone;
|
||||
private boolean startedFromProperties;
|
||||
private volatile boolean freezeRequested;
|
||||
private final AtomicReference<FlybyRequest> pendingRequest = new AtomicReference<>();
|
||||
private FlybyRequest activeRequest;
|
||||
private String[] sceneCommands = NO_COMMANDS;
|
||||
private String scenePlayerName;
|
||||
private volatile boolean sceneRequested;
|
||||
private volatile boolean sceneClearRequested;
|
||||
private int sceneSpawnTotal;
|
||||
private boolean pauseOnLostFocusSaved;
|
||||
@@ -65,8 +70,13 @@ public final class FlybyRunner {
|
||||
private static volatile boolean sceneGuarded;
|
||||
private static volatile boolean worldChangesDiscarded;
|
||||
|
||||
private double originX, originY, originZ;
|
||||
private float originYaw, originPitch;
|
||||
private double parkedX, parkedY, parkedZ;
|
||||
private float parkedYaw, parkedPitch;
|
||||
private double originX, originZ;
|
||||
private float originYaw;
|
||||
private FlybyOrigin fixedOrigin;
|
||||
private float eyeOffset;
|
||||
private double flightY;
|
||||
|
||||
private double[] pathX = NO_PATH;
|
||||
private double[] pathZ = NO_PATH;
|
||||
@@ -111,8 +121,17 @@ public final class FlybyRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
final FlybyOrigin origin;
|
||||
try {
|
||||
origin = FlybyOrigin.parse(SystemProperties.FLYBY_ORIGIN);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.error("Invalid flyby origin '{}', expected x,z[,yaw]", SystemProperties.FLYBY_ORIGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
this.startedFromProperties = true;
|
||||
this.start(configured, SystemProperties.FLYBY_LENGTH, SystemProperties.FLYBY_WARMUP_TICKS, SystemProperties.FLYBY_SPEED);
|
||||
this.fixedOrigin = origin;
|
||||
this.waitForTracy = SystemProperties.FLYBY_WAIT_FOR_TRACY;
|
||||
this.waitForFocus = SystemProperties.FLYBY_WAIT_FOR_FOCUS;
|
||||
this.exitWhenDone = SystemProperties.FLYBY_EXIT_WHEN_DONE;
|
||||
@@ -137,6 +156,7 @@ public final class FlybyRunner {
|
||||
this.waitForTracy = false;
|
||||
this.waitForFocus = false;
|
||||
this.exitWhenDone = false;
|
||||
this.fixedOrigin = null;
|
||||
sceneGuarded = false;
|
||||
this.state = State.WAITING;
|
||||
}
|
||||
@@ -160,7 +180,7 @@ public final class FlybyRunner {
|
||||
}
|
||||
if (this.state == State.IDLE || this.state == State.DONE) return;
|
||||
|
||||
if (mc.theWorld == null && (this.state == State.WARMUP || this.state == State.RUNNING || this.state == State.SETTLE)) {
|
||||
if (mc.theWorld == null && (this.state == State.PREPARING || this.state == State.WARMUP || this.state == State.RUNNING || this.state == State.SETTLE)) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
@@ -182,6 +202,12 @@ public final class FlybyRunner {
|
||||
if (this.waitForFocus && !Display.isActive()) return;
|
||||
this.begin(mc, player);
|
||||
}
|
||||
case PREPARING -> {
|
||||
final Double feetY = this.activeRequest.feetY().getNow(null);
|
||||
if (feetY == null) return;
|
||||
this.flightY = feetY + this.eyeOffset;
|
||||
this.startRun(mc);
|
||||
}
|
||||
case WARMUP -> {
|
||||
this.applyPosition(player, 0);
|
||||
if (++this.tick >= this.warmupTicks) {
|
||||
@@ -231,25 +257,49 @@ public final class FlybyRunner {
|
||||
mc.gameSettings.pauseOnLostFocus = false;
|
||||
}
|
||||
|
||||
this.originX = player.posX;
|
||||
this.originY = player.posY;
|
||||
this.originZ = player.posZ;
|
||||
this.originPitch = player.rotationPitch;
|
||||
this.parkedX = player.posX;
|
||||
this.parkedY = player.posY;
|
||||
this.parkedZ = player.posZ;
|
||||
this.parkedYaw = player.rotationYaw;
|
||||
this.parkedPitch = player.rotationPitch;
|
||||
this.eyeOffset = player.yOffset;
|
||||
|
||||
this.originYaw = Math.round(player.rotationYaw / 90.0F) * 90.0F;
|
||||
if (this.originYaw != player.rotationYaw) {
|
||||
LOGGER.info("Flyby snapped heading {} -> {}", player.rotationYaw, this.originYaw);
|
||||
final boolean creative = player.capabilities.isCreativeMode;
|
||||
if (!creative) {
|
||||
LOGGER.warn("Flyby: player is not in creative mode, flying at the player height{}", this.fixedOrigin != null ? " and ignoring the pinned origin" : "");
|
||||
}
|
||||
|
||||
if (creative && this.fixedOrigin != null) {
|
||||
this.originX = this.fixedOrigin.x();
|
||||
this.originZ = this.fixedOrigin.z();
|
||||
this.originYaw = this.fixedOrigin.yaw();
|
||||
} else {
|
||||
this.originX = player.posX;
|
||||
this.originZ = player.posZ;
|
||||
this.originYaw = Math.round(player.rotationYaw / 90.0F) * 90.0F;
|
||||
if (this.originYaw != player.rotationYaw) {
|
||||
LOGGER.info("Flyby snapped heading {} -> {}", player.rotationYaw, this.originYaw);
|
||||
}
|
||||
}
|
||||
|
||||
this.buildPath();
|
||||
this.freezeTimeAndWeather(mc);
|
||||
if (this.sceneCommands.length > 0 && mc.getIntegratedServer() != null) {
|
||||
this.scenePlayerName = player.getCommandSenderName();
|
||||
this.sceneRequested = true;
|
||||
}
|
||||
|
||||
this.suppressPacing();
|
||||
|
||||
this.tick = 0;
|
||||
if (mc.getIntegratedServer() != null) {
|
||||
final FlybyRequest request = new FlybyRequest(this.pathX, this.pathZ, this.originX, this.originZ, player.posY - this.eyeOffset, player.getCommandSenderName(), creative, this.sceneCommands.length > 0, new CompletableFuture<>());
|
||||
this.activeRequest = request;
|
||||
sceneGuarded = true;
|
||||
this.pendingRequest.set(request);
|
||||
this.state = State.PREPARING;
|
||||
} else {
|
||||
LOGGER.warn("Flyby: not singleplayer, cannot freeze time/weather or sample terrain - flying at the player height, results may not be comparable");
|
||||
this.flightY = player.posY;
|
||||
this.startRun(mc);
|
||||
}
|
||||
}
|
||||
|
||||
private void startRun(Minecraft mc) {
|
||||
this.tick = 0;
|
||||
if (this.warmupTicks > 0) {
|
||||
this.state = State.WARMUP;
|
||||
@@ -258,8 +308,7 @@ public final class FlybyRunner {
|
||||
this.beginMeasuring(mc);
|
||||
}
|
||||
|
||||
LOGGER.info("Flyby {} starting at {} {} {} yaw {}", this.route.id(),
|
||||
this.originX, this.originY, this.originZ, this.originYaw);
|
||||
LOGGER.info("Flyby {} starting at {} {} {} yaw {}", this.route.id(), this.originX, this.flightY, this.originZ, this.originYaw);
|
||||
}
|
||||
|
||||
private void suppressPacing() {
|
||||
@@ -393,12 +442,12 @@ public final class FlybyRunner {
|
||||
player.ySize = 0.0F;
|
||||
|
||||
player.prevPosX = player.lastTickPosX = this.pathX[last];
|
||||
player.prevPosY = player.lastTickPosY = this.originY;
|
||||
player.prevPosY = player.lastTickPosY = this.flightY;
|
||||
player.prevPosZ = player.lastTickPosZ = this.pathZ[last];
|
||||
player.prevRotationYaw = player.rotationYaw = this.pathYaw[last];
|
||||
player.prevRotationYawHead = player.rotationYawHead = this.pathYaw[last];
|
||||
player.prevRotationPitch = player.rotationPitch = this.originPitch;
|
||||
player.setPosition(this.pathX[last], this.originY, this.pathZ[last]);
|
||||
player.prevRotationPitch = player.rotationPitch = this.parkedPitch;
|
||||
player.setPosition(this.pathX[last], this.flightY, this.pathZ[last]);
|
||||
}
|
||||
|
||||
private void applyPosition(EntityClientPlayerMP player, int index) {
|
||||
@@ -418,37 +467,23 @@ public final class FlybyRunner {
|
||||
player.ySize = 0.0F;
|
||||
|
||||
player.prevPosX = player.lastTickPosX = prevX;
|
||||
player.prevPosY = player.lastTickPosY = this.originY;
|
||||
player.prevPosY = player.lastTickPosY = this.flightY;
|
||||
player.prevPosZ = player.lastTickPosZ = prevZ;
|
||||
|
||||
player.prevRotationYaw = prevYaw;
|
||||
player.rotationYaw = yaw;
|
||||
player.prevRotationYawHead = prevYaw;
|
||||
player.rotationYawHead = yaw;
|
||||
player.prevRotationPitch = player.rotationPitch = this.originPitch;
|
||||
player.prevRotationPitch = player.rotationPitch = this.parkedPitch;
|
||||
|
||||
player.setPosition(x, this.originY, z);
|
||||
}
|
||||
|
||||
/** Requests the freeze; the write happens on the server thread in {@link #onServerTick}. */
|
||||
private void freezeTimeAndWeather(Minecraft mc) {
|
||||
final MinecraftServer server = mc.getIntegratedServer();
|
||||
if (server == null) {
|
||||
LOGGER.warn("Flyby: not singleplayer, cannot freeze time/weather - results may not be comparable");
|
||||
return;
|
||||
}
|
||||
|
||||
sceneGuarded = true;
|
||||
this.freezeRequested = true;
|
||||
player.setPosition(x, this.flightY, z);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
final boolean clear = this.sceneClearRequested;
|
||||
final boolean freeze = this.freezeRequested;
|
||||
final boolean scene = this.sceneRequested;
|
||||
if (!clear && !freeze && !scene) return;
|
||||
if (!clear && this.pendingRequest.get() == null) return;
|
||||
|
||||
final MinecraftServer server = MinecraftServer.getServer();
|
||||
if (server == null) return;
|
||||
@@ -457,16 +492,16 @@ public final class FlybyRunner {
|
||||
this.sceneClearRequested = false;
|
||||
this.clearScene(server);
|
||||
}
|
||||
if (freeze || scene) {
|
||||
this.discardWorldChanges(server);
|
||||
}
|
||||
if (freeze) {
|
||||
this.freezeRequested = false;
|
||||
this.applyFreeze(server);
|
||||
}
|
||||
if (scene) {
|
||||
this.sceneRequested = false;
|
||||
this.runScene(server);
|
||||
|
||||
final FlybyRequest request = this.pendingRequest.getAndSet(null);
|
||||
if (request == null) return;
|
||||
|
||||
this.discardWorldChanges(server);
|
||||
final double feetY = this.flightFeetY(server, request);
|
||||
request.feetY().complete(feetY);
|
||||
this.applyFreeze(server);
|
||||
if (request.scene()) {
|
||||
this.runScene(server, request, feetY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,16 +546,52 @@ public final class FlybyRunner {
|
||||
}
|
||||
}
|
||||
|
||||
private void runScene(MinecraftServer server) {
|
||||
final EntityPlayerMP player = server.getConfigurationManager().func_152612_a(this.scenePlayerName);
|
||||
private double flightFeetY(MinecraftServer server, FlybyRequest request) {
|
||||
if (!request.sampleTerrain()) return request.parkedFeetY();
|
||||
|
||||
final EntityPlayerMP player = server.getConfigurationManager().func_152612_a(request.playerName());
|
||||
if (player == null) {
|
||||
LOGGER.warn("Flyby scene: no server player named '{}', skipping scene", this.scenePlayerName);
|
||||
LOGGER.warn("Flyby terrain: no server player named '{}', flying at the parked feet Y {}", request.playerName(), request.parkedFeetY());
|
||||
return request.parkedFeetY();
|
||||
}
|
||||
|
||||
final WorldServer world = player.getServerForPlayer();
|
||||
if (world.provider.hasNoSky) {
|
||||
LOGGER.info("Flyby terrain: dimension has a ceiling, flying at the parked feet Y {}", request.parkedFeetY());
|
||||
return request.parkedFeetY();
|
||||
}
|
||||
|
||||
final IChunkProvider chunks = world.getChunkProvider();
|
||||
final int floor = FlybyTerrain.routeFloor(request.pathX(), request.pathZ(), FlybyTerrain.CORRIDOR_RADIUS, (x, z) -> precipitationHeight(chunks, x, z));
|
||||
if (floor < 1) {
|
||||
LOGGER.info("Flyby terrain: no ground within {} blocks of the path, flying at the parked feet Y {}", FlybyTerrain.CORRIDOR_RADIUS, request.parkedFeetY());
|
||||
return request.parkedFeetY();
|
||||
}
|
||||
|
||||
final double feetY = floor + FlybyTerrain.HOVER_BLOCKS;
|
||||
LOGGER.info("Flyby terrain: route floor {} within {} blocks of the path, flight feet Y {}", floor, FlybyTerrain.CORRIDOR_RADIUS, feetY);
|
||||
if (Tracy.ENABLED) Tracy.message("flyby terrain floor=" + floor + " radius=" + FlybyTerrain.CORRIDOR_RADIUS + " feetY=" + feetY);
|
||||
return feetY;
|
||||
}
|
||||
|
||||
private static int precipitationHeight(IChunkProvider chunks, int x, int z) {
|
||||
final int chunkX = x >> 4;
|
||||
final int chunkZ = z >> 4;
|
||||
Chunk chunk = chunks.loadChunk(chunkX, chunkZ);
|
||||
if (chunk == null) chunk = chunks.provideChunk(chunkX, chunkZ);
|
||||
return chunk == null ? Integer.MIN_VALUE : chunk.getPrecipitationHeight(x & 15, z & 15);
|
||||
}
|
||||
|
||||
private void runScene(MinecraftServer server, FlybyRequest request, double feetY) {
|
||||
final EntityPlayerMP player = server.getConfigurationManager().func_152612_a(request.playerName());
|
||||
if (player == null) {
|
||||
LOGGER.warn("Flyby scene: no server player named '{}', skipping scene", request.playerName());
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearScene(server);
|
||||
|
||||
final FlybyCommandSender sender = new FlybyCommandSender(player);
|
||||
final FlybyCommandSender sender = new FlybyCommandSender(player, new ChunkCoordinates(MathHelper.floor_double(request.originX()), MathHelper.floor_double(feetY + 0.5D), MathHelper.floor_double(request.originZ())));
|
||||
for (String line : this.sceneCommands) {
|
||||
if (server.getCommandManager().executeCommand(sender, line) == 0) {
|
||||
LOGGER.warn("flyby scene command did not execute: {}", line);
|
||||
@@ -603,11 +674,11 @@ public final class FlybyRunner {
|
||||
player.motionY = 0.0D;
|
||||
player.motionZ = 0.0D;
|
||||
player.ySize = 0.0F;
|
||||
player.setPositionAndRotation(this.originX, this.originY, this.originZ, this.originYaw, this.originPitch);
|
||||
player.lastTickPosX = this.originX;
|
||||
player.lastTickPosY = this.originY;
|
||||
player.lastTickPosZ = this.originZ;
|
||||
LOGGER.info("Flyby returned to origin {} {} {}", this.originX, this.originY, this.originZ);
|
||||
player.setPositionAndRotation(this.parkedX, this.parkedY, this.parkedZ, this.parkedYaw, this.parkedPitch);
|
||||
player.lastTickPosX = this.parkedX;
|
||||
player.lastTickPosY = this.parkedY;
|
||||
player.lastTickPosZ = this.parkedZ;
|
||||
LOGGER.info("Flyby returned to {} {} {}", this.parkedX, this.parkedY, this.parkedZ);
|
||||
}
|
||||
|
||||
private String summarise(long elapsedNs, EntityClientPlayerMP player) {
|
||||
@@ -656,8 +727,8 @@ public final class FlybyRunner {
|
||||
FramePacer.endStats();
|
||||
final Minecraft mc = Minecraft.getMinecraft();
|
||||
this.restorePauseOnLostFocus(mc);
|
||||
this.freezeRequested = false;
|
||||
this.sceneRequested = false;
|
||||
this.pendingRequest.set(null);
|
||||
this.activeRequest = null;
|
||||
sceneGuarded = false;
|
||||
this.sceneClearRequested = this.sceneCommands.length > 0;
|
||||
this.restorePacing(mc);
|
||||
@@ -667,4 +738,6 @@ public final class FlybyRunner {
|
||||
public boolean startedFromProperties() {
|
||||
return this.startedFromProperties;
|
||||
}
|
||||
|
||||
private record FlybyRequest(double[] pathX, double[] pathZ, double originX, double originZ, double parkedFeetY, String playerName, boolean sampleTerrain, boolean scene, CompletableFuture<Double> feetY) {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.gtnewhorizons.angelica.debug.flyby;
|
||||
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
final class FlybyTerrain {
|
||||
|
||||
static final int CORRIDOR_RADIUS = 2;
|
||||
static final int HOVER_BLOCKS = 2;
|
||||
|
||||
private FlybyTerrain() {}
|
||||
|
||||
static int routeFloor(double[] pathX, double[] pathZ, int radius, IntBinaryOperator heights) {
|
||||
int floor = Integer.MIN_VALUE;
|
||||
int prevCellX = 0;
|
||||
int prevCellZ = 0;
|
||||
for (int i = 0; i < pathX.length; i++) {
|
||||
final int cellX = (int) Math.floor(pathX[i]);
|
||||
final int cellZ = (int) Math.floor(pathZ[i]);
|
||||
if (i > 0 && cellX == prevCellX && cellZ == prevCellZ) continue;
|
||||
prevCellX = cellX;
|
||||
prevCellZ = cellZ;
|
||||
for (int x = cellX - radius; x <= cellX + radius; x++) {
|
||||
for (int z = cellZ - radius; z <= cellZ + radius; z++) {
|
||||
floor = Math.max(floor, heights.applyAsInt(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return floor;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.gtnewhorizons.angelica.helpers;
|
||||
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
|
||||
|
||||
import net.minecraft.client.renderer.entity.RenderDragon;
|
||||
import net.minecraft.client.renderer.entity.RenderEnderman;
|
||||
import net.minecraft.client.renderer.entity.RenderSpider;
|
||||
import net.minecraft.entity.EntityLiving;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@@ -11,6 +14,10 @@ public final class RendererLivingEntityHelper {
|
||||
|
||||
private RendererLivingEntityHelper() {}
|
||||
|
||||
public static boolean hasEyePass(Object renderer) {
|
||||
return renderer instanceof RenderSpider || renderer instanceof RenderEnderman || renderer instanceof RenderDragon;
|
||||
}
|
||||
|
||||
public static String getUpsideDownName(EntityLivingBase entity) {
|
||||
if (entity instanceof EntityLiving living) {
|
||||
return living.hasCustomNameTag() ? entity.getCommandSenderName() : "";
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.iris;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.FfpExtendedAttribs;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.items.ItemRenderListManager;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
|
||||
@@ -81,6 +82,7 @@ public final class ImmediateExtendedAttribs implements ImmediateExtendedAttribHa
|
||||
|
||||
@Override
|
||||
public boolean wantsExtended() {
|
||||
GLSMHooks.resolvePendingProgram();
|
||||
return Iris.enabled && currentProgramWants != 0 && GLStateManager.getActiveProgram() != 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,6 @@ public class IrisGLSMBridge {
|
||||
private static Runnable fogDensityListener = null;
|
||||
private static Runnable colorModulatorListener = null;
|
||||
|
||||
private static boolean inputsDeferred = false;
|
||||
|
||||
private static boolean blendDeferred = false;
|
||||
|
||||
private static boolean programRestoreDeferred = false;
|
||||
|
||||
private static final Int2IntOpenHashMap programLastUpdatedFrame = new Int2IntOpenHashMap();
|
||||
@@ -269,32 +265,19 @@ public class IrisGLSMBridge {
|
||||
}
|
||||
if (!updatePipeline) return;
|
||||
|
||||
if (GLStateManager.isForeignDraw()) {
|
||||
inputsDeferred = true;
|
||||
return;
|
||||
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
|
||||
if (pipeline != null) {
|
||||
pipeline.setInputs(StateTracker.INSTANCE.getInputs());
|
||||
}
|
||||
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setInputs(StateTracker.INSTANCE.getInputs()));
|
||||
});
|
||||
|
||||
GLSMHooks.VANILLA_BLEND_CHANGE.addListener(event -> {
|
||||
if (!Iris.enabled) return;
|
||||
if (GLStateManager.isForeignDraw()) {
|
||||
blendDeferred = true;
|
||||
return;
|
||||
}
|
||||
refreshBlendCondition();
|
||||
});
|
||||
|
||||
GLSMHooks.FOREIGN_DRAW_END.addListener(event -> {
|
||||
if (!Iris.enabled) return;
|
||||
if (inputsDeferred) {
|
||||
inputsDeferred = false;
|
||||
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setInputs(StateTracker.INSTANCE.getInputs()));
|
||||
}
|
||||
if (blendDeferred) {
|
||||
blendDeferred = false;
|
||||
refreshBlendCondition();
|
||||
}
|
||||
if (programRestoreDeferred) {
|
||||
programRestoreDeferred = false;
|
||||
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
|
||||
|
||||
@@ -64,6 +64,7 @@ public enum Mixins implements IMixins {
|
||||
, "angelica.ffp.MixinTessellator_CoreProfile"
|
||||
, "angelica.glsm.MixinSplashProgressCaching"
|
||||
, "angelica.gui.MixinGuiOptions"
|
||||
, "angelica.optimizations.MixinRenderBiped_ArmorResource"
|
||||
, "angelica.optimizations.MixinRendererLivingEntity"
|
||||
, "angelica.rendering.MixinRenderGlobal_SelectionBox"
|
||||
, "angelica.gui.MixinGuiIngameForge_ModernF3"
|
||||
@@ -170,15 +171,29 @@ public enum Mixins implements IMixins {
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaConfig.enableEntityBatching)
|
||||
.addClientMixins(
|
||||
"angelica.entity.MixinItemRenderer_Instanced",
|
||||
"angelica.entity.MixinModelRenderer",
|
||||
"angelica.entity.MixinRenderGlobal_EntityBatch",
|
||||
"angelica.entity.MixinRenderItem_Instanced",
|
||||
"angelica.entity.MixinRenderManager_BatchEligibility",
|
||||
"angelica.entity.MixinRendererLivingEntity_EquippedDraws",
|
||||
"angelica.entity.MixinRendererLivingEntity_GlintClock",
|
||||
"angelica.entity.MixinRendererLivingEntity_ModelPassDraws",
|
||||
"angelica.entity.MixinRender_BatchEligibility",
|
||||
"angelica.entity.MixinRender_ShadowBatch",
|
||||
"angelica.entity.MixinTextureManager",
|
||||
"angelica.tesr.MixinTileEntitySpecialRenderer_BatchEligibility"
|
||||
)
|
||||
),
|
||||
|
||||
ANGELICA_CUBE_INSTANCING(new MixinBuilder("Capture cuboid parameters so model cubes can be drawn from a shared unit cube")
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(AngelicaConfig::cubeInstancingEnabled)
|
||||
.addClientMixins(
|
||||
"angelica.entity.MixinModelBox_CubeParams"
|
||||
)
|
||||
),
|
||||
|
||||
ANGELICA_SKIP_END_FRAME_FLUSH(new MixinBuilder("Skip the end-of-frame glFlush before the buffer swap")
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaConfig.skipEndOfFrameFlush)
|
||||
@@ -298,9 +313,9 @@ public enum Mixins implements IMixins {
|
||||
.setPhase(Phase.EARLY)
|
||||
.addClientMixins("angelica.optimizations.MixinGLAllocation")),
|
||||
|
||||
ANGELICA_DEFERRED_TESSELLATOR_BATCH(new MixinBuilder("Deferred tessellator batching for particles to reduce draw calls")
|
||||
ANGELICA_PARTICLE_BATCH(new MixinBuilder("Particle instancing, with deferred tessellator batching as the fallback")
|
||||
.setPhase(Phase.EARLY)
|
||||
.addClientMixins("angelica.particles.MixinEffectRenderer_DeferredBatch")),
|
||||
.addClientMixins("angelica.particles.MixinEffectRenderer_ParticleBatch")),
|
||||
|
||||
// Not compatible with the lwjgl debug callbacks, so disable if that's enabled
|
||||
ARCHAIC_SPLASH(new MixinBuilder()
|
||||
@@ -384,6 +399,7 @@ public enum Mixins implements IMixins {
|
||||
, "celeritas.features.mipmaps.MixinTextureMetadataSectionSerializer"
|
||||
, "celeritas.features.textures.MixinTextureMap"
|
||||
, "celeritas.features.textures.MixinTextureAtlasSprite"
|
||||
, "celeritas.features.textures.MixinStitcherHolder"
|
||||
, "celeritas.biome_blending.MixinBlockGrass"
|
||||
, "celeritas.biome_blending.MixinBlockLeaves"
|
||||
, "celeritas.biome_blending.MixinBlockLiquid"
|
||||
@@ -784,6 +800,11 @@ public enum Mixins implements IMixins {
|
||||
.setApplyIf(() -> AngelicaConfig.enableIris)
|
||||
.addClientMixins("client.dragonapi.MixinThrottleableEffectRenderer")
|
||||
),
|
||||
DRAGONAPI_PARTICLE_BATCH(new MixinBuilder("Particle instancing for DragonAPI's replacement particle renderer")
|
||||
.setPhase(Phase.LATE)
|
||||
.addRequiredMod(TargetedMod.DRAGON_API)
|
||||
.addClientMixins("client.dragonapi.MixinThrottleableEffectRenderer_ParticleBatch")
|
||||
),
|
||||
MCPATCHER_FORGE(new MixinBuilder()
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaConfig.enableMCPatcherForgeFeatures)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.mixins.interfaces;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.CubeParams;
|
||||
|
||||
public interface ModelBoxData {
|
||||
|
||||
CubeParams angelica$cubeParams();
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import net.minecraft.client.Minecraft;
|
||||
public final class BailClassCounts {
|
||||
public static final BailClassCounts MATERIAL = new BailClassCounts("tesr.bailCls.mat.", "BailMat");
|
||||
public static final BailClassCounts TEMPLATE = new BailClassCounts("tesr.bailCls.tmpl.", "BailTmpl");
|
||||
public static final BailClassCounts PARTICLE_SPILL = new BailClassCounts("particles.spillCls.", "PartSpill");
|
||||
|
||||
private final String plotPrefix;
|
||||
private final String label;
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.TerrainDrawStats;
|
||||
import com.gtnewhorizons.angelica.rendering.culling.GpuCulling;
|
||||
import com.gtnewhorizons.angelica.rendering.voxelization.SdlShadowVoxelizationSink;
|
||||
import com.gtnewhorizons.angelica.rendering.items.DroppedItemInstancer;
|
||||
import com.gtnewhorizons.angelica.rendering.particles.ParticleInstancer;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.AngelicaTesrMeshCache;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
|
||||
@@ -24,31 +26,21 @@ import org.embeddedt.embeddium.impl.render.chunk.SharedQuadIndexBuffer;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.region.RenderRegionManager;
|
||||
|
||||
public final class TracyFramePlots {
|
||||
private static long lastChunkUpdates;
|
||||
private static long lastDrawCalls, lastTexBindMisses, lastProgramSwitches, lastListPlaybacks;
|
||||
private static long lastStreamedBytes, lastStreamDraws, lastOrphanFallbacks, lastStreamContiguous;
|
||||
private static long lastRingWraps, lastVariantSwitches;
|
||||
private static long lastTesrRebuilds, lastTesrRetainedDraws, lastTesrInstDraws, lastTesrInstInstances;
|
||||
private static long lastTesrStreamedInstances, lastTesrPromotions, lastTesrCacheHits, lastTesrCacheMisses;
|
||||
private static long lastBatcherParts, lastBatcherLiveFallbacks;
|
||||
private static final long[] lastBails = new long[ModelPartBatcher.BailReason.VALUES.length];
|
||||
private static long lastSectionsUploaded, lastBytesUploaded, lastIndexBufferGrowths;
|
||||
|
||||
private static final long P_ALLOC_RATE = Tracy.plotHandle("allocRate", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_HEAP_USED = Tracy.plotHandle("heapUsed", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_CHUNK_UPDATES = Tracy.plotHandle("chunkUpdates");
|
||||
private static final PlotDelta P_CHUNK_UPDATES = new PlotDelta("chunkUpdates");
|
||||
private static final long P_ENTITIES_RENDERED = Tracy.plotHandle("entitiesRendered");
|
||||
private static final long P_ENTITIES_TOTAL = Tracy.plotHandle("entitiesTotal");
|
||||
|
||||
private static final long P_GL_DRAW_CALLS = Tracy.plotHandle("gl.drawCalls");
|
||||
private static final long P_GL_TEX_BIND_MISSES = Tracy.plotHandle("gl.texBindMisses");
|
||||
private static final long P_GL_PROGRAM_SWITCHES = Tracy.plotHandle("gl.programSwitches");
|
||||
private static final long P_GL_LIST_PLAYBACKS = Tracy.plotHandle("gl.listPlaybacks");
|
||||
private static final long P_GL_STREAMED_BYTES = Tracy.plotHandle("gl.streamedBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_GL_STREAM_DRAWS = Tracy.plotHandle("gl.streamDraws");
|
||||
private static final long P_GL_ORPHAN_FALLBACKS = Tracy.plotHandle("gl.orphanFallbacks");
|
||||
private static final long P_GL_STREAM_CONTIGUOUS = Tracy.plotHandle("gl.streamContiguous");
|
||||
private static final long P_GL_RING_WRAPS = Tracy.plotHandle("gl.ringWraps");
|
||||
private static final PlotDelta P_GL_DRAW_CALLS = new PlotDelta("gl.drawCalls");
|
||||
private static final PlotDelta P_GL_TEX_BIND_MISSES = new PlotDelta("gl.texBindMisses");
|
||||
private static final PlotDelta P_GL_PROGRAM_SWITCHES = new PlotDelta("gl.programSwitches");
|
||||
private static final PlotDelta P_GL_LIST_PLAYBACKS = new PlotDelta("gl.listPlaybacks");
|
||||
private static final PlotDelta P_GL_STREAMED_BYTES = new PlotDelta("gl.streamedBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final PlotDelta P_GL_STREAM_DRAWS = new PlotDelta("gl.streamDraws");
|
||||
private static final PlotDelta P_GL_ORPHAN_FALLBACKS = new PlotDelta("gl.orphanFallbacks");
|
||||
private static final PlotDelta P_GL_STREAM_CONTIGUOUS = new PlotDelta("gl.streamContiguous");
|
||||
private static final PlotDelta P_GL_RING_WRAPS = new PlotDelta("gl.ringWraps");
|
||||
private static final long P_GL_SECTION_META_BYTES = Tracy.plotHandle("gl.sectionMetaBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
|
||||
private static final long P_SHADOW_RELAY_FRAMES = Tracy.plotHandle("shadow.relayFrames");
|
||||
@@ -59,7 +51,7 @@ public final class TracyFramePlots {
|
||||
private static final long P_VOX_DISPATCHES = Tracy.plotHandle("voxelization.dispatches");
|
||||
private static final long P_VOX_REGIONS = Tracy.plotHandle("voxelization.regions");
|
||||
private static final long P_DRAW_BATCH_REBUILDS = Tracy.plotHandle("draw.batchRebuilds");
|
||||
private static final long P_DRAW_INDEX_BUFFER_GROWTHS = Tracy.plotHandle("draw.indexBufferGrowths");
|
||||
private static final PlotDelta P_DRAW_INDEX_BUFFER_GROWTHS = new PlotDelta("draw.indexBufferGrowths");
|
||||
|
||||
private static final long P_CULL_COMMANDS_SUBMITTED = Tracy.plotHandle("cull.commandsSubmitted");
|
||||
private static final long P_CULL_REGIONS_DRAWN = Tracy.plotHandle("cull.regionsDrawn");
|
||||
@@ -79,24 +71,37 @@ public final class TracyFramePlots {
|
||||
private static final long P_FFP_STAGED_CLIP_PLANES = Tracy.plotHandle("ffp.stagedClipPlanes");
|
||||
private static final long P_FFP_STAGED_MISC = Tracy.plotHandle("ffp.stagedMisc");
|
||||
private static final long P_FFP_PROGRAMS = Tracy.plotHandle("ffp.programs");
|
||||
private static final long P_FFP_VARIANT_SWITCHES = Tracy.plotHandle("ffp.variantSwitches");
|
||||
private static final PlotDelta P_FFP_VARIANT_SWITCHES = new PlotDelta("ffp.variantSwitches");
|
||||
|
||||
private static final long P_TESR_REBUILDS = Tracy.plotHandle("tesr.rebuilds");
|
||||
private static final long P_TESR_RETAINED_DRAWS = Tracy.plotHandle("tesr.retainedDraws");
|
||||
private static final long P_TESR_INSTANCED_DRAWS = Tracy.plotHandle("tesr.instancedDraws");
|
||||
private static final long P_TESR_INSTANCED_INSTANCES = Tracy.plotHandle("tesr.instancedInstances");
|
||||
private static final long P_TESR_STREAMED_INSTANCES = Tracy.plotHandle("tesr.streamedInstances");
|
||||
private static final long P_TESR_VOLATILE_PROMOTIONS = Tracy.plotHandle("tesr.volatilePromotions");
|
||||
private static final PlotDelta P_TESR_REBUILDS = new PlotDelta("tesr.rebuilds");
|
||||
private static final PlotDelta P_TESR_RETAINED_DRAWS = new PlotDelta("tesr.retainedDraws");
|
||||
private static final PlotDelta P_TESR_INSTANCED_DRAWS = new PlotDelta("tesr.instancedDraws");
|
||||
private static final PlotDelta P_TESR_INSTANCED_INSTANCES = new PlotDelta("tesr.instancedInstances");
|
||||
private static final PlotDelta P_TESR_CUBE_INSTANCES = new PlotDelta("tesr.cubeInstances");
|
||||
private static final PlotDelta P_TESR_TEXMAT_RUNS = new PlotDelta("tesr.texMatrixRuns");
|
||||
private static final PlotDelta P_TESR_STREAMED_INSTANCES = new PlotDelta("tesr.streamedInstances");
|
||||
private static final PlotDelta P_TESR_VOLATILE_PROMOTIONS = new PlotDelta("tesr.volatilePromotions");
|
||||
private static final long P_TESR_RETAINED_BYTES = Tracy.plotHandle("tesr.retainedBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_TESR_BUFFER_SOURCE_BYTES = Tracy.plotHandle("tesr.bufferSourceBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_TESR_CACHE_HITS = Tracy.plotHandle("tesr.cacheHits");
|
||||
private static final long P_TESR_CACHE_MISSES = Tracy.plotHandle("tesr.cacheMisses");
|
||||
private static final long P_TESR_MODEL_PARTS = Tracy.plotHandle("tesr.modelParts");
|
||||
private static final long P_TESR_LIVE_FALLBACKS = Tracy.plotHandle("tesr.liveFallbacks");
|
||||
private static final long[] P_TESR_BAILS = bailHandles();
|
||||
private static final PlotDelta P_TESR_CACHE_HITS = new PlotDelta("tesr.cacheHits");
|
||||
private static final PlotDelta P_TESR_CACHE_MISSES = new PlotDelta("tesr.cacheMisses");
|
||||
private static final PlotDelta P_TESR_MODEL_PARTS = new PlotDelta("tesr.modelParts");
|
||||
private static final PlotDelta P_TESR_LIVE_FALLBACKS = new PlotDelta("tesr.liveFallbacks");
|
||||
private static final PlotDelta P_ITEMS_INSTANCED = new PlotDelta("items.instanced");
|
||||
private static final PlotDelta P_ITEMS_FALLBACK = new PlotDelta("items.fallback");
|
||||
private static final PlotDelta P_ITEMS_GLINT_INSTANCED = new PlotDelta("items.glintInstanced");
|
||||
private static final PlotDelta P_PARTICLES_DIRECT = new PlotDelta("particles.direct");
|
||||
private static final PlotDelta P_PARTICLES_CAPTURED = new PlotDelta("particles.captured");
|
||||
private static final PlotDelta P_PARTICLES_SPILLED = new PlotDelta("particles.spilled");
|
||||
private static final PlotDelta P_PARTICLES_UNDECODABLE = new PlotDelta("particles.undecodable");
|
||||
private static final PlotDelta P_PARTICLES_DRAWS = new PlotDelta("particles.draws");
|
||||
private static final PlotDelta[] TESR_BAILS = bailHandles();
|
||||
private static final PlotDelta[] ITEM_BAILS = itemBailHandles();
|
||||
private static final PlotDelta P_ENTITY_SHADOW_QUADS = new PlotDelta("entity.shadowQuads");
|
||||
private static final PlotDelta P_ENTITY_SHADOW_DRAWS = new PlotDelta("entity.shadowDraws");
|
||||
|
||||
private static final long P_MESH_UPLOADED_SECTIONS = Tracy.plotHandle("mesh.uploadedSections");
|
||||
private static final long P_MESH_UPLOADED_BYTES = Tracy.plotHandle("mesh.uploadedBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final PlotDelta P_MESH_UPLOADED_SECTIONS = new PlotDelta("mesh.uploadedSections");
|
||||
private static final PlotDelta P_MESH_UPLOADED_BYTES = new PlotDelta("mesh.uploadedBytes", TracyBackend.PLOT_FORMAT_MEMORY);
|
||||
private static final long P_MESH_META_SLOTS = Tracy.plotHandle("mesh.metaSlots");
|
||||
private static final long P_MT_QUEUE_DEPTH = Tracy.plotHandle("mtQueue.depth");
|
||||
private static final long P_MT_QUEUE_TASKS = Tracy.plotHandle("mtQueue.tasksRan");
|
||||
@@ -110,10 +115,17 @@ public final class TracyFramePlots {
|
||||
private static final long P_DYN_UPDATES = Tracy.plotHandle("dyn.updates");
|
||||
private static final long P_DYN_PENDING_REBUILDS = Tracy.plotHandle("dyn.pendingRebuilds");
|
||||
|
||||
private static long[] bailHandles() {
|
||||
private static PlotDelta[] bailHandles() {
|
||||
final ModelPartBatcher.BailReason[] reasons = ModelPartBatcher.BailReason.VALUES;
|
||||
final long[] out = new long[reasons.length];
|
||||
for (int i = 0; i < reasons.length; i++) out[i] = Tracy.plotHandle(reasons[i].plotName);
|
||||
final PlotDelta[] out = new PlotDelta[reasons.length];
|
||||
for (int i = 0; i < reasons.length; i++) out[i] = new PlotDelta(reasons[i].plotName);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static PlotDelta[] itemBailHandles() {
|
||||
final DroppedItemInstancer.BailReason[] reasons = DroppedItemInstancer.BailReason.VALUES;
|
||||
final PlotDelta[] out = new PlotDelta[reasons.length];
|
||||
for (int i = 0; i < reasons.length; i++) out[i] = new PlotDelta(reasons[i].plotName);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -153,9 +165,7 @@ public final class TracyFramePlots {
|
||||
Tracy.plotInt(P_PACER_LOCKED, FramePacer.locked() ? 1 : 0);
|
||||
Tracy.plotInt(P_PACER_PROBING, FramePacer.probing() ? 1 : 0);
|
||||
|
||||
final int chunksUpdated = WorldRenderer.chunksUpdated;
|
||||
Tracy.plotInt(P_CHUNK_UPDATES, chunksUpdated - lastChunkUpdates);
|
||||
lastChunkUpdates = chunksUpdated;
|
||||
P_CHUNK_UPDATES.plot(WorldRenderer.chunksUpdated);
|
||||
if (mc.renderGlobal != null) {
|
||||
Tracy.plotInt(P_ENTITIES_RENDERED, mc.renderGlobal.countEntitiesRendered);
|
||||
}
|
||||
@@ -163,15 +173,15 @@ public final class TracyFramePlots {
|
||||
Tracy.plotInt(P_ENTITIES_TOTAL, mc.theWorld.loadedEntityList.size());
|
||||
}
|
||||
|
||||
lastDrawCalls = delta(P_GL_DRAW_CALLS, GLStateManager.drawCalls, lastDrawCalls);
|
||||
lastTexBindMisses = delta(P_GL_TEX_BIND_MISSES, GLStateManager.texBindMisses, lastTexBindMisses);
|
||||
lastProgramSwitches = delta(P_GL_PROGRAM_SWITCHES, GLStateManager.programSwitches, lastProgramSwitches);
|
||||
lastListPlaybacks = delta(P_GL_LIST_PLAYBACKS, DisplayListManager.listPlaybacks, lastListPlaybacks);
|
||||
lastStreamedBytes = delta(P_GL_STREAMED_BYTES, TessellatorStreamingDrawer.streamedBytes, lastStreamedBytes);
|
||||
lastStreamDraws = delta(P_GL_STREAM_DRAWS, TessellatorStreamingDrawer.streamDraws, lastStreamDraws);
|
||||
lastOrphanFallbacks = delta(P_GL_ORPHAN_FALLBACKS, TessellatorStreamingDrawer.orphanFallbacks, lastOrphanFallbacks);
|
||||
lastStreamContiguous = delta(P_GL_STREAM_CONTIGUOUS, TessellatorStreamingDrawer.streamContiguous, lastStreamContiguous);
|
||||
lastRingWraps = delta(P_GL_RING_WRAPS, TessellatorStreamingDrawer.ringWraps(), lastRingWraps);
|
||||
P_GL_DRAW_CALLS.plot(GLStateManager.drawCalls);
|
||||
P_GL_TEX_BIND_MISSES.plot(GLStateManager.texBindMisses);
|
||||
P_GL_PROGRAM_SWITCHES.plot(GLStateManager.programSwitches);
|
||||
P_GL_LIST_PLAYBACKS.plot(DisplayListManager.listPlaybacks);
|
||||
P_GL_STREAMED_BYTES.plot(TessellatorStreamingDrawer.streamedBytes);
|
||||
P_GL_STREAM_DRAWS.plot(TessellatorStreamingDrawer.streamDraws);
|
||||
P_GL_ORPHAN_FALLBACKS.plot(TessellatorStreamingDrawer.orphanFallbacks);
|
||||
P_GL_STREAM_CONTIGUOUS.plot(TessellatorStreamingDrawer.streamContiguous);
|
||||
P_GL_RING_WRAPS.plot(TessellatorStreamingDrawer.ringWraps());
|
||||
Tracy.plotInt(P_GL_SECTION_META_BYTES, TerrainDrawStats.takeSectionMetaBytes());
|
||||
Tracy.plotInt(P_SHADOW_RELAY_FRAMES, ShadowRenderer.SHADOW_TERRAIN_RELAID ? 1 : 0);
|
||||
Tracy.plotInt(P_TERRAIN_DRAW_COMMANDS, TerrainDrawStats.takeCommandsSubmitted());
|
||||
@@ -199,33 +209,47 @@ public final class TracyFramePlots {
|
||||
Tracy.plotInt(P_FFP_STAGED_CLIP_PLANES, ffp.statLastFrameStagedClipPlanes());
|
||||
Tracy.plotInt(P_FFP_STAGED_MISC, ffp.statLastFrameStagedMisc());
|
||||
Tracy.plotInt(P_FFP_PROGRAMS, ffp.statProgramCount());
|
||||
lastVariantSwitches = delta(P_FFP_VARIANT_SWITCHES, ShaderManager.variantSwitches, lastVariantSwitches);
|
||||
P_FFP_VARIANT_SWITCHES.plot(ShaderManager.variantSwitches);
|
||||
|
||||
final TesrBatchRenderer tesr = TesrBatchRenderer.INSTANCE;
|
||||
final ModelPartBatcher batcher = ModelPartBatcher.INSTANCE;
|
||||
|
||||
lastTesrRebuilds = delta(P_TESR_REBUILDS, tesr.statRebuilds(), lastTesrRebuilds);
|
||||
lastTesrRetainedDraws = delta(P_TESR_RETAINED_DRAWS, tesr.statRetainedDraws(), lastTesrRetainedDraws);
|
||||
lastTesrInstDraws = delta(P_TESR_INSTANCED_DRAWS, tesr.statInstancedDraws(), lastTesrInstDraws);
|
||||
lastTesrInstInstances = delta(P_TESR_INSTANCED_INSTANCES, tesr.statInstancedInstances(), lastTesrInstInstances);
|
||||
lastTesrStreamedInstances = delta(P_TESR_STREAMED_INSTANCES, tesr.statStreamedInstances(), lastTesrStreamedInstances);
|
||||
lastTesrPromotions = delta(P_TESR_VOLATILE_PROMOTIONS, tesr.statVolatilePromotions(), lastTesrPromotions);
|
||||
P_TESR_REBUILDS.plot(tesr.statRebuilds());
|
||||
P_TESR_RETAINED_DRAWS.plot(tesr.statRetainedDraws());
|
||||
P_TESR_INSTANCED_DRAWS.plot(tesr.statInstancedDraws() + batcher.statInstancedDraws());
|
||||
P_TESR_INSTANCED_INSTANCES.plot(tesr.statInstancedInstances() + batcher.statInstancedInstances());
|
||||
P_TESR_CUBE_INSTANCES.plot(batcher.statCubeInstances());
|
||||
P_TESR_TEXMAT_RUNS.plot(batcher.statTexMatrixRuns());
|
||||
P_TESR_STREAMED_INSTANCES.plot(tesr.statStreamedInstances());
|
||||
P_TESR_VOLATILE_PROMOTIONS.plot(tesr.statVolatilePromotions());
|
||||
if (plots) {
|
||||
Tracy.plotInt(P_TESR_RETAINED_BYTES, tesr.statRetainedBytes());
|
||||
Tracy.plotInt(P_TESR_BUFFER_SOURCE_BYTES, tesr.statBufferSourceBytes());
|
||||
}
|
||||
lastTesrCacheHits = delta(P_TESR_CACHE_HITS, AngelicaTesrMeshCache.cacheHits, lastTesrCacheHits);
|
||||
lastTesrCacheMisses = delta(P_TESR_CACHE_MISSES, AngelicaTesrMeshCache.cacheMisses, lastTesrCacheMisses);
|
||||
lastBatcherParts = delta(P_TESR_MODEL_PARTS, batcher.statParts(), lastBatcherParts);
|
||||
lastBatcherLiveFallbacks = delta(P_TESR_LIVE_FALLBACKS, batcher.statLiveFallbacks(), lastBatcherLiveFallbacks);
|
||||
P_TESR_CACHE_HITS.plot(AngelicaTesrMeshCache.cacheHits);
|
||||
P_TESR_CACHE_MISSES.plot(AngelicaTesrMeshCache.cacheMisses);
|
||||
P_TESR_MODEL_PARTS.plot(batcher.statParts());
|
||||
P_TESR_LIVE_FALLBACKS.plot(batcher.statLiveFallbacks());
|
||||
P_ITEMS_INSTANCED.plot(DroppedItemInstancer.statInstanced());
|
||||
P_ITEMS_FALLBACK.plot(DroppedItemInstancer.statFallback());
|
||||
P_ITEMS_GLINT_INSTANCED.plot(DroppedItemInstancer.statGlintInstanced());
|
||||
for (final DroppedItemInstancer.BailReason reason : DroppedItemInstancer.BailReason.VALUES) {
|
||||
ITEM_BAILS[reason.ordinal()].plot(DroppedItemInstancer.statBail(reason));
|
||||
}
|
||||
P_PARTICLES_DIRECT.plot(ParticleInstancer.getDirect());
|
||||
P_PARTICLES_CAPTURED.plot(ParticleInstancer.getCaptured());
|
||||
P_PARTICLES_SPILLED.plot(ParticleInstancer.getSpilled());
|
||||
P_PARTICLES_UNDECODABLE.plot(ParticleInstancer.getUndecodable());
|
||||
P_PARTICLES_DRAWS.plot(ParticleInstancer.getDraws());
|
||||
P_ENTITY_SHADOW_QUADS.plot(batcher.statShadowQuads());
|
||||
P_ENTITY_SHADOW_DRAWS.plot(batcher.statShadowDraws());
|
||||
for (final ModelPartBatcher.BailReason reason : ModelPartBatcher.BailReason.VALUES) {
|
||||
final int i = reason.ordinal();
|
||||
lastBails[i] = delta(P_TESR_BAILS[i], batcher.statBail(reason), lastBails[i]);
|
||||
TESR_BAILS[reason.ordinal()].plot(batcher.statBail(reason));
|
||||
}
|
||||
|
||||
lastSectionsUploaded = delta(P_MESH_UPLOADED_SECTIONS, RenderRegionManager.getSectionsUploaded(), lastSectionsUploaded);
|
||||
lastBytesUploaded = delta(P_MESH_UPLOADED_BYTES, RenderRegionManager.getBytesUploaded(), lastBytesUploaded);
|
||||
lastIndexBufferGrowths = delta(P_DRAW_INDEX_BUFFER_GROWTHS, SharedQuadIndexBuffer.getGrowths(), lastIndexBufferGrowths);
|
||||
P_MESH_UPLOADED_SECTIONS.plot(RenderRegionManager.getSectionsUploaded());
|
||||
P_MESH_UPLOADED_BYTES.plot(RenderRegionManager.getBytesUploaded());
|
||||
P_DRAW_INDEX_BUFFER_GROWTHS.plot(SharedQuadIndexBuffer.getGrowths());
|
||||
Tracy.plotInt(P_MESH_META_SLOTS, GpuCulling.sectionMeta().getHighWaterMark());
|
||||
Tracy.plotInt(P_MT_QUEUE_DEPTH, AngelicaRenderQueue.getQueueDepth());
|
||||
Tracy.plotInt(P_MT_QUEUE_TASKS, AngelicaRenderQueue.getLastFrameTasksRan());
|
||||
@@ -253,10 +277,24 @@ public final class TracyFramePlots {
|
||||
RenderClassTimings.TESR.flushFrame(plots);
|
||||
BailClassCounts.MATERIAL.flushFrame(plots);
|
||||
BailClassCounts.TEMPLATE.flushFrame(plots);
|
||||
BailClassCounts.PARTICLE_SPILL.flushFrame(plots);
|
||||
}
|
||||
|
||||
private static long delta(long handle, long now, long last) {
|
||||
Tracy.plotInt(handle, now - last);
|
||||
return now;
|
||||
private static final class PlotDelta {
|
||||
private final long handle;
|
||||
private long last;
|
||||
|
||||
PlotDelta(String name) {
|
||||
this(name, TracyBackend.PLOT_FORMAT_NUMBER);
|
||||
}
|
||||
|
||||
PlotDelta(String name, int format) {
|
||||
handle = Tracy.plotHandle(name, format);
|
||||
}
|
||||
|
||||
void plot(long now) {
|
||||
Tracy.plotInt(handle, now - last);
|
||||
last = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
/**
|
||||
* The shape of the drawn disc, how it splits into wedges, and how far out each kind of geometry should
|
||||
* really be worth building.
|
||||
*/
|
||||
final class CloudDisc {
|
||||
|
||||
static final int CELLS_PER_CHUNK = 8;
|
||||
static final int MARGIN_CELLS = 16;
|
||||
static final int WEDGE_COUNT = 32;
|
||||
static final double WEDGES_PER_RADIAN = WEDGE_COUNT / (2.0 * Math.PI);
|
||||
static final int ALWAYS_DRAWN_CELLS = Math.max(CELLS_PER_CHUNK, 4 * MARGIN_CELLS);
|
||||
private static final double WALL_CUT_PIXELS = 0.625;
|
||||
private static final double PLATE_LOD_PIXELS = 0.35;
|
||||
private static final double TAN_11_25_DEG = 0.19891236737965800; // √(4+2√2)−√2−1
|
||||
private static final double TAN_22_5_DEG = 0.41421356237309503; // √2−1
|
||||
private static final double TAN_33_75_DEG = 0.66817863791929892; // √(4−2√2)+1−√2
|
||||
private static final int DISTANCE_STEP_CELLS = 32;
|
||||
|
||||
static {
|
||||
if (WEDGE_COUNT != 32) {
|
||||
throw new AssertionError("wedgeOf splits four quadrants into eight; WEDGE_COUNT must be 32");
|
||||
}
|
||||
}
|
||||
|
||||
private CloudDisc() {
|
||||
}
|
||||
|
||||
static double cellCoordinate(double worldPosition, float cellWidthBlocks, double offset) {
|
||||
return worldPosition / cellWidthBlocks + offset;
|
||||
}
|
||||
|
||||
static float textureOffset(int anchor, int textureSize) {
|
||||
return Math.floorMod(anchor, textureSize) * (1.0f / textureSize);
|
||||
}
|
||||
|
||||
static boolean withinMargin(int driftX, int driftZ) {
|
||||
// Reject distant anchors before squaring
|
||||
return driftX >= -MARGIN_CELLS && driftX <= MARGIN_CELLS && driftZ >= -MARGIN_CELLS && driftZ <= MARGIN_CELLS && driftX * driftX + driftZ * driftZ <= MARGIN_CELLS * MARGIN_CELLS;
|
||||
}
|
||||
|
||||
static boolean withinDisc(long driftX, long driftZ, int radiusCells) {
|
||||
return driftX >= -radiusCells && driftX <= radiusCells && driftZ >= -radiusCells && driftZ <= radiusCells && driftX * driftX + driftZ * driftZ <= (long) radiusCells * radiusCells;
|
||||
}
|
||||
|
||||
static double pixelsPerRadian(int displayHeight, float fovDegrees) {
|
||||
if (!(fovDegrees > 0.0f) || fovDegrees >= 180.0f) return 0.0;
|
||||
return (displayHeight * 0.5) / Math.tan(Math.toRadians(fovDegrees) * 0.5);
|
||||
}
|
||||
|
||||
static int wallCutCells(double pixelsPerRadian, boolean platesInFront) {
|
||||
if (!platesInFront || pixelsPerRadian <= 0.0) return Integer.MAX_VALUE;
|
||||
|
||||
return roundUpToStep(pixelsPerRadian / (3.0 * WALL_CUT_PIXELS));
|
||||
}
|
||||
|
||||
static int plateLodCells(double pixelsPerRadian, float cloudBaseRelativeY, float cellWidthBlocks, boolean coarseCoverUsable) {
|
||||
if (!coarseCoverUsable || pixelsPerRadian <= 0.0) return Integer.MAX_VALUE;
|
||||
|
||||
final double heightBlocks = Math.max(1.0, Math.abs(cloudBaseRelativeY));
|
||||
return roundUpToStep(Math.sqrt(heightBlocks * pixelsPerRadian / (cellWidthBlocks * PLATE_LOD_PIXELS)));
|
||||
}
|
||||
|
||||
private static int roundUpToStep(double cells) {
|
||||
final long steps = (long) (cells / DISTANCE_STEP_CELLS) + 1;
|
||||
return (int) Math.min(steps * DISTANCE_STEP_CELLS, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
static int wedgeOf(int x, int z) {
|
||||
int foldedX = -x;
|
||||
int foldedZ = -z;
|
||||
if (foldedX == 0 && foldedZ == 0) return WEDGE_COUNT / 2;
|
||||
|
||||
final int quadrant;
|
||||
if (foldedX > 0 && foldedZ >= 0) {
|
||||
quadrant = 0;
|
||||
} else if (foldedX <= 0 && foldedZ > 0) {
|
||||
quadrant = 1;
|
||||
final int oldX = foldedX;
|
||||
foldedX = foldedZ;
|
||||
foldedZ = -oldX;
|
||||
} else if (foldedX < 0) {
|
||||
quadrant = 2;
|
||||
foldedX = -foldedX;
|
||||
foldedZ = -foldedZ;
|
||||
} else {
|
||||
quadrant = 3;
|
||||
final int oldX = foldedX;
|
||||
foldedX = -foldedZ;
|
||||
foldedZ = oldX;
|
||||
}
|
||||
|
||||
final int withinQuadrant;
|
||||
if (foldedZ < foldedX) {
|
||||
if (foldedZ < TAN_11_25_DEG * foldedX) withinQuadrant = 0;
|
||||
else if (foldedZ < TAN_22_5_DEG * foldedX) withinQuadrant = 1;
|
||||
else if (foldedZ < TAN_33_75_DEG * foldedX) withinQuadrant = 2;
|
||||
else withinQuadrant = 3;
|
||||
} else {
|
||||
if (foldedX > TAN_33_75_DEG * foldedZ) withinQuadrant = 4;
|
||||
else if (foldedX > TAN_22_5_DEG * foldedZ) withinQuadrant = 5;
|
||||
else if (foldedX > TAN_11_25_DEG * foldedZ) withinQuadrant = 6;
|
||||
else withinQuadrant = 7;
|
||||
}
|
||||
|
||||
return quadrant * 8 + withinQuadrant;
|
||||
}
|
||||
|
||||
static double driftAngle(double driftCells) {
|
||||
final double nearestCulledCells = ALWAYS_DRAWN_CELLS - driftCells;
|
||||
return nearestCulledCells <= driftCells ? Math.PI / 2.0 : Math.asin(driftCells / nearestCulledCells);
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
final class CloudEdges {
|
||||
private static final int CELL_EMPTY_THRESHOLD = 10;
|
||||
|
||||
final int width, height;
|
||||
private final int wMask, hMask;
|
||||
private final boolean whPow2;
|
||||
private final long[] opaqueBits;
|
||||
final int opaqueCount;
|
||||
|
||||
CloudEdges(int width, int height, ByteBuffer rgba) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.wMask = width - 1;
|
||||
this.hMask = height - 1;
|
||||
this.whPow2 = (width & wMask) == 0 && (height & hMask) == 0;
|
||||
final int total = width * height;
|
||||
final long[] bits = new long[(total + 63) >>> 6];
|
||||
int op = 0;
|
||||
for (int i = 0; i < total; i++) {
|
||||
if ((rgba.get(i * 4 + 3) & 0xFF) >= CELL_EMPTY_THRESHOLD) {
|
||||
bits[i >>> 6] |= 1L << (i & 63);
|
||||
op++;
|
||||
}
|
||||
}
|
||||
this.opaqueBits = bits;
|
||||
this.opaqueCount = op;
|
||||
}
|
||||
|
||||
boolean cellOpaque(int x, int z) {
|
||||
final int xw, zh;
|
||||
if (whPow2) {
|
||||
xw = x & wMask;
|
||||
zh = z & hMask;
|
||||
} else {
|
||||
xw = Math.floorMod(x, width);
|
||||
zh = Math.floorMod(z, height);
|
||||
}
|
||||
final int idx = xw + zh * width;
|
||||
return ((opaqueBits[idx >>> 6] >>> (idx & 63)) & 1L) != 0L;
|
||||
}
|
||||
|
||||
long readOpaqueRowBits(int x0, int z) {
|
||||
final int zMod, x0Mod;
|
||||
if (whPow2) {
|
||||
zMod = z & hMask;
|
||||
x0Mod = x0 & wMask;
|
||||
} else {
|
||||
zMod = Math.floorMod(z, height);
|
||||
x0Mod = Math.floorMod(x0, width);
|
||||
}
|
||||
if (x0Mod + 10 <= width) {
|
||||
return readContigBits(opaqueBits, (long) zMod * width + x0Mod, 10);
|
||||
}
|
||||
final int colsBeforeWrap = width - x0Mod;
|
||||
final long firstPart = readContigBits(opaqueBits, (long) zMod * width + x0Mod, colsBeforeWrap);
|
||||
final long secondPart = readContigBits(opaqueBits, (long) zMod * width, 10 - colsBeforeWrap);
|
||||
return firstPart | (secondPart << colsBeforeWrap);
|
||||
}
|
||||
|
||||
private static long readContigBits(long[] bits, long startBitIdx, int count) {
|
||||
final int wordIdx = (int) (startBitIdx >>> 6);
|
||||
final int bitOff = (int) (startBitIdx & 63);
|
||||
final long mask = count == 64 ? -1L : (1L << count) - 1L;
|
||||
final long w0 = bits[wordIdx];
|
||||
if (bitOff + count <= 64) {
|
||||
return (w0 >>> bitOff) & mask;
|
||||
}
|
||||
final long w1 = bits[wordIdx + 1];
|
||||
return ((w0 >>> bitOff) | (w1 << (64 - bitOff))) & mask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.ALWAYS_DRAWN_CELLS;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.MARGIN_CELLS;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.WEDGE_COUNT;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.wedgeOf;
|
||||
|
||||
/**
|
||||
* Walls as one instanced face each.
|
||||
*/
|
||||
final class CloudFaceMesh {
|
||||
|
||||
static final int DIR_DOWN = 0, DIR_UP = 1, DIR_NORTH = 2, DIR_SOUTH = 3, DIR_WEST = 4, DIR_EAST = 5;
|
||||
static final int CELL_LIMIT = 511;
|
||||
private static final int FLAG_INSIDE = 1;
|
||||
private static final int FACE_ATTRIB = 0;
|
||||
/** cellX 10b | cellZ 10b | dir 3b | flags 2b. */
|
||||
private static final int FACE_BYTES = 4;
|
||||
private static final int INTERIOR_FACES = 6;
|
||||
private static final int BUCKET_COUNT = WEDGE_COUNT + 1;
|
||||
private static final int BUCKET_ALWAYS_DRAWN = 0;
|
||||
private static final int BUCKET_WALL_WEDGE = 1;
|
||||
|
||||
private static final Tracy.ZoneId Z_STAMP_WALLS = Tracy.zoneId("cloudFaceStampWalls", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_UPLOAD = Tracy.zoneId("cloudFaceUpload", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_REORDER = Tracy.zoneId("cloudFaceReorder", Tracy.COLOR_CLIENT);
|
||||
private final int[] wallStart = new int[WEDGE_COUNT + 1];
|
||||
private final int[] plateStart = new int[WEDGE_COUNT + 1];
|
||||
private int vao = -1;
|
||||
private int vbo = -1;
|
||||
private int ebo = -1;
|
||||
private int vboCapacityBytes;
|
||||
private int uploadedCount;
|
||||
private int stampedCount;
|
||||
private int interiorCount;
|
||||
private int[] faceData = new int[4096];
|
||||
private int[] faceSortKey = new int[4096];
|
||||
private int[] sortKeyStart = new int[0];
|
||||
private int ringsPerBucket = 1;
|
||||
private int buildAnchorX = Integer.MIN_VALUE, buildAnchorZ = Integer.MIN_VALUE;
|
||||
private int orderAnchorX = Integer.MIN_VALUE, orderAnchorZ = Integer.MIN_VALUE;
|
||||
private CloudShape shape;
|
||||
private ByteBuffer uploadBuffer;
|
||||
|
||||
int uploadedCount() {
|
||||
return uploadedCount;
|
||||
}
|
||||
|
||||
int interiorCount() {
|
||||
return interiorCount;
|
||||
}
|
||||
|
||||
int buildAnchorX() {
|
||||
return buildAnchorX;
|
||||
}
|
||||
|
||||
int buildAnchorZ() {
|
||||
return buildAnchorZ;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
uploadedCount = 0;
|
||||
interiorCount = 0;
|
||||
}
|
||||
|
||||
void build(CloudShape shape, int anchorX, int anchorZ, int radiusCells, int radiusCellsSq, int wallCutCells) {
|
||||
stampedCount = 0;
|
||||
this.shape = shape;
|
||||
ringsPerBucket = 2 * (radiusCells + MARGIN_CELLS) + 1;
|
||||
buildAnchorX = anchorX;
|
||||
buildAnchorZ = anchorZ;
|
||||
orderAnchorX = anchorX;
|
||||
orderAnchorZ = anchorZ;
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_STAMP_WALLS);
|
||||
addWalls(shape, anchorX, anchorZ, radiusCells, radiusCellsSq, wallCutCells);
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_UPLOAD);
|
||||
finishBuild();
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
|
||||
private static int packFace(int cellX, int cellZ, int dir, int flags) {
|
||||
return ((cellX + 512) & 0x3FF)
|
||||
| (((cellZ + 512) & 0x3FF) << 10)
|
||||
| (dir << 20)
|
||||
| (flags << 23);
|
||||
}
|
||||
|
||||
private void addFace(int cellX, int cellZ, int dir, int bucket, int ring) {
|
||||
if (stampedCount + INTERIOR_FACES == faceSortKey.length) {
|
||||
faceSortKey = Arrays.copyOf(faceSortKey, faceSortKey.length * 2);
|
||||
faceData = Arrays.copyOf(faceData, faceData.length * 2);
|
||||
}
|
||||
faceData[stampedCount] = packFace(cellX, cellZ, dir, 0);
|
||||
faceSortKey[stampedCount] = bucket * ringsPerBucket + ring;
|
||||
stampedCount++;
|
||||
}
|
||||
|
||||
private void stampInterior(int anchorX, int anchorZ) {
|
||||
interiorCount = 0;
|
||||
if (shape == null || (shape.cellFlagsAt(anchorX, anchorZ) & CloudShape.OPAQUE) == 0) return;
|
||||
|
||||
final int driftX = anchorX - buildAnchorX;
|
||||
final int driftZ = anchorZ - buildAnchorZ;
|
||||
for (int dir = DIR_DOWN; dir <= DIR_EAST; dir++) {
|
||||
faceData[uploadedCount + dir] = packFace(driftX, driftZ, dir, FLAG_INSIDE);
|
||||
}
|
||||
interiorCount = INTERIOR_FACES;
|
||||
}
|
||||
|
||||
private void addWalls(CloudShape shape, int anchorX, int anchorZ, int radiusCells, int radiusCellsSq, int wallCutCells) {
|
||||
stampWallRuns(shape, shape.westRuns, DIR_WEST, true, anchorX, anchorZ, radiusCells, radiusCellsSq, wallCutCells);
|
||||
stampWallRuns(shape, shape.eastRuns, DIR_EAST, true, anchorX, anchorZ, radiusCells, radiusCellsSq, wallCutCells);
|
||||
stampWallRuns(shape, shape.northRuns, DIR_NORTH, false, anchorX, anchorZ, radiusCells, radiusCellsSq, wallCutCells);
|
||||
stampWallRuns(shape, shape.southRuns, DIR_SOUTH, false, anchorX, anchorZ, radiusCells, radiusCellsSq, wallCutCells);
|
||||
}
|
||||
|
||||
private void stampWallRuns(CloudShape shape, int[] runs, int dir, boolean alongZ, int anchorX, int anchorZ,
|
||||
int radiusCells, int radiusCellsSq, int wallCutCells) {
|
||||
final int texW = shape.width;
|
||||
final int texH = shape.height;
|
||||
final int minCell = -radiusCells;
|
||||
final int maxCell = radiusCells;
|
||||
final int nearAlwaysSq = ALWAYS_DRAWN_CELLS * ALWAYS_DRAWN_CELLS;
|
||||
final int wallCutSq = wallCutCells >= radiusCells ? Integer.MAX_VALUE : wallCutCells * wallCutCells;
|
||||
|
||||
for (int r = 0; r < runs.length; r += 3) {
|
||||
final int runLength = runs[r + 2];
|
||||
final int baseX = Math.floorMod(runs[r] - anchorX, texW);
|
||||
final int baseZ = Math.floorMod(runs[r + 1] - anchorZ, texH);
|
||||
|
||||
for (int kz = Math.floorDiv(minCell - baseZ - (alongZ ? runLength - 1 : 0), texH); kz <= Math.floorDiv(maxCell - baseZ, texH); kz++) {
|
||||
final int runZ = baseZ + kz * texH;
|
||||
for (int kx = Math.floorDiv(minCell - baseX - (alongZ ? 0 : runLength - 1), texW); kx <= Math.floorDiv(maxCell - baseX, texW); kx++) {
|
||||
final int runX = baseX + kx * texW;
|
||||
|
||||
for (int step = 0; step < runLength; step++) {
|
||||
final int cellX = alongZ ? runX : runX + step;
|
||||
final int cellZ = alongZ ? runZ + step : runZ;
|
||||
|
||||
if (dir == DIR_WEST ? cellX < 1 - MARGIN_CELLS
|
||||
: dir == DIR_EAST ? cellX > MARGIN_CELLS - 1
|
||||
: dir == DIR_NORTH ? cellZ < 1 - MARGIN_CELLS
|
||||
: cellZ > MARGIN_CELLS - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int distSq = cellX * cellX + cellZ * cellZ;
|
||||
if (distSq > radiusCellsSq) continue;
|
||||
final boolean near = distSq <= nearAlwaysSq;
|
||||
if (!near && distSq > wallCutSq) continue;
|
||||
|
||||
final int bucket = near ? BUCKET_ALWAYS_DRAWN : BUCKET_WALL_WEDGE + wedgeOf(cellX, cellZ);
|
||||
addFace(cellX, cellZ, dir, bucket, Math.abs(cellX) + Math.abs(cellZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean orderStale(int anchorX, int anchorZ) {
|
||||
return anchorX != orderAnchorX || anchorZ != orderAnchorZ;
|
||||
}
|
||||
|
||||
void reorder(int anchorX, int anchorZ) {
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_REORDER);
|
||||
try {
|
||||
final int driftX = anchorX - buildAnchorX;
|
||||
final int driftZ = anchorZ - buildAnchorZ;
|
||||
final int nearAlwaysSq = ALWAYS_DRAWN_CELLS * ALWAYS_DRAWN_CELLS;
|
||||
for (int i = 0; i < uploadedCount; i++) {
|
||||
final int packed = faceData[i];
|
||||
final int cellX = (packed & 0x3FF) - 512 - driftX;
|
||||
final int cellZ = ((packed >> 10) & 0x3FF) - 512 - driftZ;
|
||||
final int distSq = cellX * cellX + cellZ * cellZ;
|
||||
final int bucket = distSq <= nearAlwaysSq ? BUCKET_ALWAYS_DRAWN : BUCKET_WALL_WEDGE + wedgeOf(cellX, cellZ);
|
||||
final int ring = Math.min(Math.abs(cellX) + Math.abs(cellZ), ringsPerBucket - 1);
|
||||
faceSortKey[i] = bucket * ringsPerBucket + ring;
|
||||
}
|
||||
stampInterior(anchorX, anchorZ);
|
||||
sortAndUpload();
|
||||
orderAnchorX = anchorX;
|
||||
orderAnchorZ = anchorZ;
|
||||
} finally {
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishBuild() {
|
||||
uploadedCount = stampedCount;
|
||||
stampInterior(buildAnchorX, buildAnchorZ);
|
||||
if (uploadedCount == 0) Arrays.fill(wallStart, 0);
|
||||
sortAndUpload();
|
||||
}
|
||||
|
||||
private void sortAndUpload() {
|
||||
final int sortKeyCount = BUCKET_COUNT * ringsPerBucket;
|
||||
if (sortKeyStart.length < sortKeyCount + 1) sortKeyStart = new int[sortKeyCount + 1];
|
||||
else Arrays.fill(sortKeyStart, 0, sortKeyCount + 1, 0);
|
||||
|
||||
for (int i = 0; i < uploadedCount; i++) sortKeyStart[faceSortKey[i]]++;
|
||||
int acc = 0;
|
||||
for (int k = 0; k < sortKeyCount; k++) {
|
||||
final int keyTotal = sortKeyStart[k];
|
||||
sortKeyStart[k] = acc;
|
||||
acc += keyTotal;
|
||||
}
|
||||
sortKeyStart[sortKeyCount] = acc;
|
||||
|
||||
for (int w = 0; w < WEDGE_COUNT; w++) wallStart[w] = sortKeyStart[(BUCKET_WALL_WEDGE + w) * ringsPerBucket];
|
||||
wallStart[WEDGE_COUNT] = uploadedCount;
|
||||
|
||||
final int totalFaces = uploadedCount + interiorCount;
|
||||
if (totalFaces == 0) return;
|
||||
|
||||
final int uploadBytes = totalFaces * FACE_BYTES;
|
||||
if (uploadBuffer == null || uploadBuffer.capacity() < uploadBytes) {
|
||||
uploadBuffer = BufferUtils.createByteBuffer(Math.max(uploadBytes, 8192));
|
||||
}
|
||||
uploadBuffer.clear();
|
||||
final IntBuffer ints = uploadBuffer.asIntBuffer();
|
||||
for (int i = 0; i < uploadedCount; i++) {
|
||||
ints.put(sortKeyStart[faceSortKey[i]]++, faceData[i]);
|
||||
}
|
||||
for (int i = 0; i < interiorCount; i++) {
|
||||
ints.put(uploadedCount + i, faceData[uploadedCount + i]);
|
||||
}
|
||||
uploadBuffer.position(0).limit(uploadBytes);
|
||||
|
||||
final boolean freshBuffers = vbo == -1;
|
||||
if (freshBuffers) vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
if (uploadBytes > vboCapacityBytes) {
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, uploadBuffer, GL15.GL_DYNAMIC_DRAW);
|
||||
vboCapacityBytes = uploadBytes;
|
||||
} else {
|
||||
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0, uploadBuffer);
|
||||
}
|
||||
|
||||
if (freshBuffers) {
|
||||
ensureQuadIndices();
|
||||
if (vao == -1) vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glEnableVertexAttribArray(FACE_ATTRIB);
|
||||
GLStateManager.glVertexAttribDivisor(FACE_ATTRIB, 1);
|
||||
GLStateManager.glVertexAttribIPointer(FACE_ATTRIB, 1, GL11.GL_INT, FACE_BYTES, 0L);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
private void ensureQuadIndices() {
|
||||
if (ebo != -1) return;
|
||||
ebo = GLStateManager.glGenBuffers();
|
||||
final IntBuffer indices = BufferUtils.createIntBuffer(6);
|
||||
indices.put(0).put(1).put(2).put(0).put(2).put(3);
|
||||
indices.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indices, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
}
|
||||
|
||||
void bind() {
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
void drawRange(int startFace, int endFace) {
|
||||
final int faces = endFace - startFace;
|
||||
if (faces <= 0) return;
|
||||
GLStateManager.glVertexAttribIPointer(FACE_ATTRIB, 1, GL11.GL_INT, FACE_BYTES, (long) startFace * FACE_BYTES);
|
||||
GLStateManager.glDrawElementsInstanced(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_INT, 0L, faces);
|
||||
}
|
||||
|
||||
void drawInterior() {
|
||||
drawRange(uploadedCount, uploadedCount + interiorCount);
|
||||
}
|
||||
|
||||
int[] plateStarts() {
|
||||
return plateStart;
|
||||
}
|
||||
|
||||
int[] wallStarts() {
|
||||
return wallStart;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.ALWAYS_DRAWN_CELLS;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.WEDGE_COUNT;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.wedgeOf;
|
||||
|
||||
/**
|
||||
* The plate rectangles that land inside the disc for one anchor.
|
||||
*/
|
||||
final class CloudPlateCover {
|
||||
|
||||
private final int[] wedgeBucketCount = new int[WEDGE_COUNT + 2];
|
||||
private int[] rects = new int[4096 * 4];
|
||||
private int[] rectWedgeBucket = new int[4096];
|
||||
private int[] orderedRects = new int[4096];
|
||||
private int count;
|
||||
private boolean groupByWedge = true;
|
||||
|
||||
int count() {
|
||||
return count;
|
||||
}
|
||||
|
||||
int orderedRect(int order) {
|
||||
return orderedRects[order];
|
||||
}
|
||||
|
||||
int minX(int rect) {
|
||||
return rects[rect * 4];
|
||||
}
|
||||
|
||||
int minZ(int rect) {
|
||||
return rects[rect * 4 + 1];
|
||||
}
|
||||
|
||||
int maxX(int rect) {
|
||||
return rects[rect * 4 + 2];
|
||||
}
|
||||
|
||||
int maxZ(int rect) {
|
||||
return rects[rect * 4 + 3];
|
||||
}
|
||||
|
||||
int wedgeBucket(int rect) {
|
||||
return rectWedgeBucket[rect];
|
||||
}
|
||||
|
||||
void build(CloudShape shape, int anchorX, int anchorZ, int radiusCells, int radiusCellsSq, int lodCells, boolean groupByWedge) {
|
||||
count = 0;
|
||||
this.groupByWedge = groupByWedge;
|
||||
Arrays.fill(wedgeBucketCount, 0);
|
||||
|
||||
final int texW = shape.width;
|
||||
final int texH = shape.height;
|
||||
final int[] coarseRects = shape.coarsePlateRects;
|
||||
|
||||
if (coarseRects == null || lodCells >= radiusCells) {
|
||||
stampRects(shape.plateRects, anchorX, anchorZ, texW, texH, radiusCells, radiusCellsSq, 0, 0, -1, -1, false);
|
||||
} else {
|
||||
final int fineX0 = 2 * Math.floorDiv(anchorX - lodCells, 2) - anchorX;
|
||||
final int fineZ0 = 2 * Math.floorDiv(anchorZ - lodCells, 2) - anchorZ;
|
||||
final int fineX1 = 2 * Math.floorDiv(anchorX + lodCells, 2) + 1 - anchorX;
|
||||
final int fineZ1 = 2 * Math.floorDiv(anchorZ + lodCells, 2) + 1 - anchorZ;
|
||||
stampRects(shape.plateRects, anchorX, anchorZ, texW, texH, radiusCells, radiusCellsSq, fineX0, fineZ0, fineX1, fineZ1, false);
|
||||
stampRects(coarseRects, anchorX, anchorZ, texW, texH, radiusCells, radiusCellsSq, fineX0, fineZ0, fineX1, fineZ1, true);
|
||||
}
|
||||
|
||||
if (groupByWedge) orderByWedge();
|
||||
}
|
||||
|
||||
private void stampRects(int[] sourceRects, int anchorX, int anchorZ, int texW, int texH, int radiusCells, int radiusCellsSq,
|
||||
int fineX0, int fineZ0, int fineX1, int fineZ1, boolean outsideFineBox) {
|
||||
final int minCell = -radiusCells;
|
||||
final int maxCell = radiusCells;
|
||||
|
||||
for (int r = 0; r < sourceRects.length; r += 4) {
|
||||
final int rectW = sourceRects[r + 2];
|
||||
final int rectH = sourceRects[r + 3];
|
||||
final int baseX = Math.floorMod(sourceRects[r] - anchorX, texW);
|
||||
final int baseZ = Math.floorMod(sourceRects[r + 1] - anchorZ, texH);
|
||||
|
||||
for (int kz = Math.floorDiv(minCell - rectH + 1 - baseZ, texH); kz <= Math.floorDiv(maxCell - baseZ, texH); kz++) {
|
||||
final int cellZ0 = Math.max(baseZ + kz * texH, minCell);
|
||||
final int cellZ1 = Math.min(baseZ + kz * texH + rectH - 1, maxCell);
|
||||
if (cellZ0 > cellZ1) continue;
|
||||
|
||||
for (int kx = Math.floorDiv(minCell - rectW + 1 - baseX, texW); kx <= Math.floorDiv(maxCell - baseX, texW); kx++) {
|
||||
final int cellX0 = Math.max(baseX + kx * texW, minCell);
|
||||
final int cellX1 = Math.min(baseX + kx * texW + rectW - 1, maxCell);
|
||||
if (cellX0 > cellX1) continue;
|
||||
|
||||
final boolean touchesFineBox = fineX1 >= fineX0
|
||||
&& cellX1 >= fineX0 && cellX0 <= fineX1 && cellZ1 >= fineZ0 && cellZ0 <= fineZ1;
|
||||
|
||||
if (fineX1 < fineX0) {
|
||||
clipToDisc(cellX0, cellZ0, cellX1, cellZ1, radiusCellsSq);
|
||||
} else if (!touchesFineBox) {
|
||||
if (outsideFineBox) clipToDisc(cellX0, cellZ0, cellX1, cellZ1, radiusCellsSq);
|
||||
} else if (!outsideFineBox) {
|
||||
clipToDisc(Math.max(cellX0, fineX0), Math.max(cellZ0, fineZ0),
|
||||
Math.min(cellX1, fineX1), Math.min(cellZ1, fineZ1), radiusCellsSq);
|
||||
} else {
|
||||
clipToDisc(cellX0, cellZ0, cellX1, Math.min(cellZ1, fineZ0 - 1), radiusCellsSq);
|
||||
clipToDisc(cellX0, Math.max(cellZ0, fineZ1 + 1), cellX1, cellZ1, radiusCellsSq);
|
||||
final int midZ0 = Math.max(cellZ0, fineZ0);
|
||||
final int midZ1 = Math.min(cellZ1, fineZ1);
|
||||
clipToDisc(cellX0, midZ0, Math.min(cellX1, fineX0 - 1), midZ1, radiusCellsSq);
|
||||
clipToDisc(Math.max(cellX0, fineX1 + 1), midZ0, cellX1, midZ1, radiusCellsSq);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clipToDisc(int cellX0, int cellZ0, int cellX1, int cellZ1, int radiusCellsSq) {
|
||||
if (cellX0 > cellX1 || cellZ0 > cellZ1) return;
|
||||
|
||||
final int farthestX = Math.max(Math.abs(cellX0), Math.abs(cellX1));
|
||||
final int farthestZ = Math.max(Math.abs(cellZ0), Math.abs(cellZ1));
|
||||
if (farthestX * farthestX + farthestZ * farthestZ <= radiusCellsSq) {
|
||||
addRect(cellX0, cellZ0, cellX1, cellZ1);
|
||||
return;
|
||||
}
|
||||
|
||||
final int nearestX = (cellX0 <= 0 && cellX1 >= 0) ? 0 : Math.min(Math.abs(cellX0), Math.abs(cellX1));
|
||||
final int nearestZ = (cellZ0 <= 0 && cellZ1 >= 0) ? 0 : Math.min(Math.abs(cellZ0), Math.abs(cellZ1));
|
||||
if (nearestX * nearestX + nearestZ * nearestZ > radiusCellsSq) return;
|
||||
|
||||
for (int cellZ = cellZ0; cellZ <= cellZ1; cellZ++) {
|
||||
final int halfWidth = (int) Math.sqrt(radiusCellsSq - cellZ * cellZ);
|
||||
final int rowX0 = Math.max(cellX0, -halfWidth);
|
||||
final int rowX1 = Math.min(cellX1, halfWidth);
|
||||
if (rowX0 > rowX1) continue;
|
||||
addRect(rowX0, cellZ, rowX1, cellZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void addRect(int cellX0, int cellZ0, int cellX1, int cellZ1) {
|
||||
int bucket = 0;
|
||||
if (groupByWedge) {
|
||||
final int nearestX = (cellX0 <= 0 && cellX1 >= 0) ? 0 : Math.min(Math.abs(cellX0), Math.abs(cellX1));
|
||||
final int nearestZ = (cellZ0 <= 0 && cellZ1 >= 0) ? 0 : Math.min(Math.abs(cellZ0), Math.abs(cellZ1));
|
||||
final boolean near = nearestX * nearestX + nearestZ * nearestZ <= ALWAYS_DRAWN_CELLS * ALWAYS_DRAWN_CELLS;
|
||||
|
||||
final int wedge = wedgeOf(cellX0, cellZ0);
|
||||
if (!near
|
||||
&& wedge == wedgeOf(cellX1 + 1, cellZ0) && wedge == wedgeOf(cellX0, cellZ1 + 1) && wedge == wedgeOf(cellX1 + 1, cellZ1 + 1)) {
|
||||
bucket = wedge + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count * 4 + 4 > rects.length) {
|
||||
rects = Arrays.copyOf(rects, rects.length * 2);
|
||||
rectWedgeBucket = Arrays.copyOf(rectWedgeBucket, rectWedgeBucket.length * 2);
|
||||
}
|
||||
final int slot = count * 4;
|
||||
rects[slot] = cellX0;
|
||||
rects[slot + 1] = cellZ0;
|
||||
rects[slot + 2] = cellX1;
|
||||
rects[slot + 3] = cellZ1;
|
||||
rectWedgeBucket[count] = bucket;
|
||||
wedgeBucketCount[bucket]++;
|
||||
count++;
|
||||
}
|
||||
|
||||
private void orderByWedge() {
|
||||
int acc = 0;
|
||||
for (int b = 0; b <= WEDGE_COUNT + 1; b++) {
|
||||
final int bucketTotal = wedgeBucketCount[b];
|
||||
wedgeBucketCount[b] = acc;
|
||||
acc += bucketTotal;
|
||||
}
|
||||
if (orderedRects.length < count) orderedRects = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
orderedRects[wedgeBucketCount[rectWedgeBucket[i]]++] = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,193 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Takes {@code clouds.png}, figures out which cells are cloud cells, precomputes greedy rectangles for the
|
||||
* plates, and merges the exposed edges into wall runs.
|
||||
*/
|
||||
final class CloudShape {
|
||||
static final int OPAQUE = 1 << 4;
|
||||
static final int WEST_EMPTY = 1;
|
||||
static final int EAST_EMPTY = 1 << 1;
|
||||
static final int NORTH_EMPTY = 1 << 2;
|
||||
static final int SOUTH_EMPTY = 1 << 3;
|
||||
private static final int MIN_OPAQUE_ALPHA = 10;
|
||||
final int width, height;
|
||||
final int[] plateRects;
|
||||
final int[] coarsePlateRects;
|
||||
final int[] westRuns, eastRuns, northRuns, southRuns;
|
||||
final boolean opaqueTexelsAllWhite;
|
||||
private final int widthMask, heightMask;
|
||||
private final boolean sizeIsPowerOfTwo;
|
||||
private final byte[] cellFlags;
|
||||
|
||||
CloudShape(int width, int height, ByteBuffer rgba) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.widthMask = width - 1;
|
||||
this.heightMask = height - 1;
|
||||
this.sizeIsPowerOfTwo = (width & widthMask) == 0 && (height & heightMask) == 0;
|
||||
final int cellCount = width * height;
|
||||
final boolean[] opaque = new boolean[cellCount];
|
||||
boolean allWhite = true;
|
||||
for (int i = 0; i < cellCount; i++) {
|
||||
final int alpha = rgba.get(i * 4 + 3) & 0xFF;
|
||||
if (alpha < MIN_OPAQUE_ALPHA) continue;
|
||||
opaque[i] = true;
|
||||
if (allWhite && (alpha != 0xFF
|
||||
|| (rgba.get(i * 4) & 0xFF) != 0xFF
|
||||
|| (rgba.get(i * 4 + 1) & 0xFF) != 0xFF
|
||||
|| (rgba.get(i * 4 + 2) & 0xFF) != 0xFF)) {
|
||||
allWhite = false;
|
||||
}
|
||||
}
|
||||
this.opaqueTexelsAllWhite = allWhite;
|
||||
|
||||
final byte[] cellFlags = new byte[cellCount];
|
||||
for (int z = 0; z < height; z++) {
|
||||
final int rowStart = z * width;
|
||||
final int northRowStart = Math.floorMod(z - 1, height) * width;
|
||||
final int southRowStart = Math.floorMod(z + 1, height) * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (!opaque[rowStart + x]) continue;
|
||||
int flagBits = OPAQUE;
|
||||
if (!opaque[rowStart + Math.floorMod(x - 1, width)]) flagBits |= WEST_EMPTY;
|
||||
if (!opaque[rowStart + Math.floorMod(x + 1, width)]) flagBits |= EAST_EMPTY;
|
||||
if (!opaque[northRowStart + x]) flagBits |= NORTH_EMPTY;
|
||||
if (!opaque[southRowStart + x]) flagBits |= SOUTH_EMPTY;
|
||||
cellFlags[rowStart + x] = (byte) flagBits;
|
||||
}
|
||||
}
|
||||
this.cellFlags = cellFlags;
|
||||
this.plateRects = buildPlateRects(opaque, width, height);
|
||||
this.coarsePlateRects = (width % 2 == 0 && height % 2 == 0) ? buildCoarsePlateRects(opaque, width, height) : null;
|
||||
|
||||
this.westRuns = buildRunsAlongZ(cellFlags, width, height, WEST_EMPTY);
|
||||
this.eastRuns = buildRunsAlongZ(cellFlags, width, height, EAST_EMPTY);
|
||||
this.northRuns = buildRunsAlongX(cellFlags, width, height, NORTH_EMPTY);
|
||||
this.southRuns = buildRunsAlongX(cellFlags, width, height, SOUTH_EMPTY);
|
||||
}
|
||||
|
||||
private static int[] buildPlateRects(boolean[] opaque, int width, int height) {
|
||||
final boolean[] uncovered = opaque.clone();
|
||||
int[] rects = new int[256];
|
||||
int count = 0;
|
||||
|
||||
for (int z = 0; z < height; z++) {
|
||||
final int rowStart = z * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (!uncovered[rowStart + x]) continue;
|
||||
|
||||
int rectW = 1;
|
||||
while (x + rectW < width && uncovered[rowStart + x + rectW]) rectW++;
|
||||
|
||||
int rectH = 1;
|
||||
extend:
|
||||
while (z + rectH < height) {
|
||||
final int nextRowStart = (z + rectH) * width;
|
||||
for (int dx = 0; dx < rectW; dx++) {
|
||||
if (!uncovered[nextRowStart + x + dx]) break extend;
|
||||
}
|
||||
rectH++;
|
||||
}
|
||||
|
||||
for (int dz = 0; dz < rectH; dz++) {
|
||||
final int coveredRowStart = (z + dz) * width;
|
||||
for (int dx = 0; dx < rectW; dx++) uncovered[coveredRowStart + x + dx] = false;
|
||||
}
|
||||
|
||||
if (count * 4 + 4 > rects.length) rects = Arrays.copyOf(rects, rects.length * 2);
|
||||
rects[count * 4] = x;
|
||||
rects[count * 4 + 1] = z;
|
||||
rects[count * 4 + 2] = rectW;
|
||||
rects[count * 4 + 3] = rectH;
|
||||
count++;
|
||||
x += rectW - 1;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOf(rects, count * 4);
|
||||
}
|
||||
|
||||
private static int[] buildCoarsePlateRects(boolean[] opaque, int width, int height) {
|
||||
final int coarseWidth = width / 2;
|
||||
final int coarseHeight = height / 2;
|
||||
final boolean[] coarseOpaque = new boolean[coarseWidth * coarseHeight];
|
||||
for (int z = 0; z < coarseHeight; z++) {
|
||||
final int topRowStart = (z * 2) * width;
|
||||
final int bottomRowStart = (z * 2 + 1) * width;
|
||||
for (int x = 0; x < coarseWidth; x++) {
|
||||
int filled = 0;
|
||||
if (opaque[topRowStart + x * 2]) filled++;
|
||||
if (opaque[topRowStart + x * 2 + 1]) filled++;
|
||||
if (opaque[bottomRowStart + x * 2]) filled++;
|
||||
if (opaque[bottomRowStart + x * 2 + 1]) filled++;
|
||||
coarseOpaque[z * coarseWidth + x] = filled >= 2;
|
||||
}
|
||||
}
|
||||
|
||||
final int[] rects = buildPlateRects(coarseOpaque, coarseWidth, coarseHeight);
|
||||
for (int i = 0; i < rects.length; i++) rects[i] *= 2;
|
||||
return rects;
|
||||
}
|
||||
|
||||
private static int[] buildRunsAlongZ(byte[] cellFlags, int width, int height, int emptyBit) {
|
||||
int[] runs = new int[192];
|
||||
int count = 0;
|
||||
for (int x = 0; x < width; x++) {
|
||||
int z = 0;
|
||||
while (z < height) {
|
||||
if ((cellFlags[x + z * width] & emptyBit) == 0) {
|
||||
z++;
|
||||
continue;
|
||||
}
|
||||
int runLength = 1;
|
||||
while (z + runLength < height && (cellFlags[x + (z + runLength) * width] & emptyBit) != 0) runLength++;
|
||||
if (count * 3 + 3 > runs.length) runs = Arrays.copyOf(runs, runs.length * 2);
|
||||
runs[count * 3] = x;
|
||||
runs[count * 3 + 1] = z;
|
||||
runs[count * 3 + 2] = runLength;
|
||||
count++;
|
||||
z += runLength;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOf(runs, count * 3);
|
||||
}
|
||||
|
||||
private static int[] buildRunsAlongX(byte[] cellFlags, int width, int height, int emptyBit) {
|
||||
int[] runs = new int[192];
|
||||
int count = 0;
|
||||
for (int z = 0; z < height; z++) {
|
||||
final int rowStart = z * width;
|
||||
int x = 0;
|
||||
while (x < width) {
|
||||
if ((cellFlags[rowStart + x] & emptyBit) == 0) {
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
int runLength = 1;
|
||||
while (x + runLength < width && (cellFlags[rowStart + x + runLength] & emptyBit) != 0) runLength++;
|
||||
if (count * 3 + 3 > runs.length) runs = Arrays.copyOf(runs, runs.length * 2);
|
||||
runs[count * 3] = x;
|
||||
runs[count * 3 + 1] = z;
|
||||
runs[count * 3 + 2] = runLength;
|
||||
count++;
|
||||
x += runLength;
|
||||
}
|
||||
}
|
||||
return Arrays.copyOf(runs, count * 3);
|
||||
}
|
||||
|
||||
int cellFlagsAt(int x, int z) {
|
||||
final int wrappedX, wrappedZ;
|
||||
if (sizeIsPowerOfTwo) {
|
||||
wrappedX = x & widthMask;
|
||||
wrappedZ = z & heightMask;
|
||||
} else {
|
||||
wrappedX = Math.floorMod(x, width);
|
||||
wrappedZ = Math.floorMod(z, height);
|
||||
}
|
||||
return cellFlags[wrappedX + wrappedZ * width] & 0xFF;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,71 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import org.embeddedt.embeddium.impl.gl.shader.ShaderBindingContext;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat4v;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformInt;
|
||||
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix4f;
|
||||
|
||||
/**
|
||||
* The uniform handles both cloud programs bind.
|
||||
*/
|
||||
final class CloudUniforms {
|
||||
static final float ALPHA = 0.8f;
|
||||
|
||||
final GlUniformMatrix4f mvp;
|
||||
final GlUniformMatrix4f mv;
|
||||
final GlUniformMatrix4f modelView;
|
||||
final GlUniformFloat4v colorMult;
|
||||
final GlUniformFloat4v fogParams;
|
||||
final GlUniformFloat4v fogColor;
|
||||
final GlUniformInt fogEnabled;
|
||||
final GlUniformInt tex;
|
||||
|
||||
final GlUniformInt textureUnit;
|
||||
final GlUniformFloat cellHeight;
|
||||
final GlUniformFloat4v scroll;
|
||||
private final float[] vec4Buf = new float[4];
|
||||
private float lastScrollX = Float.NaN, lastScrollZ = Float.NaN;
|
||||
private float lastTextureScaleX = Float.NaN, lastTextureScaleZ = Float.NaN;
|
||||
private int lastFogEnabled = -1;
|
||||
private float lastFogR = Float.NaN, lastFogG = Float.NaN, lastFogB = Float.NaN;
|
||||
private float lastColorR = Float.NaN, lastColorG = Float.NaN, lastColorB = Float.NaN;
|
||||
private float lastFogParamX = Float.NaN, lastFogParamY = Float.NaN, lastFogParamZ = Float.NaN, lastFogParamW = Float.NaN;
|
||||
private final float[] uploadBuf = new float[4];
|
||||
private float lastCellHeight = Float.NaN;
|
||||
|
||||
CloudUniforms(ShaderBindingContext ctx) {
|
||||
mvp = ctx.bindUniform("u_MVPMatrix", GlUniformMatrix4f::new);
|
||||
mv = ctx.bindUniform("u_MVMatrix", GlUniformMatrix4f::new);
|
||||
colorMult = ctx.bindUniform("u_ColorMult", GlUniformFloat4v::new);
|
||||
fogParams = ctx.bindUniform("u_FogParams", GlUniformFloat4v::new);
|
||||
fogColor = ctx.bindUniform("u_FogColor", GlUniformFloat4v::new);
|
||||
fogEnabled = ctx.bindUniform("u_FogEnabled", GlUniformInt::new);
|
||||
tex = ctx.bindUniform("u_Tex", GlUniformInt::new);
|
||||
CloudUniforms(ShaderBindingContext context) {
|
||||
mvp = context.bindUniform("u_MVPMatrix", GlUniformMatrix4f::new);
|
||||
modelView = context.bindUniform("u_MVMatrix", GlUniformMatrix4f::new);
|
||||
colorMult = context.bindUniform("u_ColorMult", GlUniformFloat4v::new);
|
||||
fogParams = context.bindUniform("u_FogParams", GlUniformFloat4v::new);
|
||||
fogColor = context.bindUniform("u_FogColor", GlUniformFloat4v::new);
|
||||
fogEnabled = context.bindUniform("u_FogEnabled", GlUniformInt::new);
|
||||
textureUnit = context.bindUniformIfPresent("u_Tex", GlUniformInt::new);
|
||||
cellHeight = context.bindUniformIfPresent("u_CellHeight", GlUniformFloat::new);
|
||||
scroll = context.bindUniformIfPresent("u_Scroll", GlUniformFloat4v::new);
|
||||
}
|
||||
|
||||
void setScroll(float x, float z, float textureScaleX, float textureScaleZ) {
|
||||
if (scroll == null || (x == lastScrollX && z == lastScrollZ && textureScaleX == lastTextureScaleX && textureScaleZ == lastTextureScaleZ)) return;
|
||||
lastScrollX = x;
|
||||
lastScrollZ = z;
|
||||
lastTextureScaleX = textureScaleX;
|
||||
lastTextureScaleZ = textureScaleZ;
|
||||
vec4Buf[0] = x;
|
||||
vec4Buf[1] = z;
|
||||
vec4Buf[2] = textureScaleX;
|
||||
vec4Buf[3] = textureScaleZ;
|
||||
scroll.set(vec4Buf);
|
||||
}
|
||||
|
||||
void setCellHeight(float blocks) {
|
||||
if (blocks == lastCellHeight) return;
|
||||
lastCellHeight = blocks;
|
||||
cellHeight.setFloat(blocks);
|
||||
}
|
||||
|
||||
void setFogEnabled(boolean enabled) {
|
||||
final int val = enabled ? 1 : 0;
|
||||
if (val != lastFogEnabled) {
|
||||
lastFogEnabled = val;
|
||||
fogEnabled.setInt(val);
|
||||
final int enabledValue = enabled ? 1 : 0;
|
||||
if (enabledValue != lastFogEnabled) {
|
||||
lastFogEnabled = enabledValue;
|
||||
fogEnabled.setInt(enabledValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +74,11 @@ final class CloudUniforms {
|
||||
lastFogR = r;
|
||||
lastFogG = g;
|
||||
lastFogB = b;
|
||||
uploadBuf[0] = r;
|
||||
uploadBuf[1] = g;
|
||||
uploadBuf[2] = b;
|
||||
uploadBuf[3] = 1.0f;
|
||||
fogColor.set(uploadBuf);
|
||||
vec4Buf[0] = r;
|
||||
vec4Buf[1] = g;
|
||||
vec4Buf[2] = b;
|
||||
vec4Buf[3] = 1.0f;
|
||||
fogColor.set(vec4Buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,11 +88,11 @@ final class CloudUniforms {
|
||||
lastFogParamY = y;
|
||||
lastFogParamZ = z;
|
||||
lastFogParamW = w;
|
||||
uploadBuf[0] = x;
|
||||
uploadBuf[1] = y;
|
||||
uploadBuf[2] = z;
|
||||
uploadBuf[3] = w;
|
||||
fogParams.set(uploadBuf);
|
||||
vec4Buf[0] = x;
|
||||
vec4Buf[1] = y;
|
||||
vec4Buf[2] = z;
|
||||
vec4Buf[3] = w;
|
||||
fogParams.set(vec4Buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +101,11 @@ final class CloudUniforms {
|
||||
lastColorR = r;
|
||||
lastColorG = g;
|
||||
lastColorB = b;
|
||||
uploadBuf[0] = r;
|
||||
uploadBuf[1] = g;
|
||||
uploadBuf[2] = b;
|
||||
uploadBuf[3] = ALPHA;
|
||||
colorMult.set(uploadBuf);
|
||||
vec4Buf[0] = r;
|
||||
vec4Buf[1] = g;
|
||||
vec4Buf[2] = b;
|
||||
vec4Buf[3] = ALPHA;
|
||||
colorMult.set(vec4Buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,971 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VertexBufferType;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.ALWAYS_DRAWN_CELLS;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.WEDGE_COUNT;
|
||||
import static com.gtnewhorizons.angelica.render.CloudDisc.wedgeOf;
|
||||
import static org.joml.Math.clamp;
|
||||
|
||||
/**
|
||||
* Plates and walls as vertices.
|
||||
*/
|
||||
final class CloudVertexMesh {
|
||||
|
||||
static final byte FACE_DOWN = 0, FACE_UP = 1, FACE_SIDE_X = 2, FACE_SIDE_Z = 3;
|
||||
static final float[] FACE_SHADE = {0.7f, 1.0f, 0.9f, 0.8f};
|
||||
private static final int[] FACE_SHADE_PACKED;
|
||||
private static final int FACE_SHADE_ATTRIB = 5;
|
||||
private static final int STRIDE_WITH_UV = 24;
|
||||
private static final int NORMAL_OFFSET_WITH_UV = 20;
|
||||
private static final int STRIDE_NO_UV = 16;
|
||||
private static final int NORMAL_OFFSET_NO_UV = 12;
|
||||
private static final float EDGE_OVERLAP = 0.0001f;
|
||||
private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN;
|
||||
private static final int NORMAL_POS_X = packNormal((byte) 127, (byte) 0, (byte) 0);
|
||||
private static final int NORMAL_NEG_X = packNormal((byte) -127, (byte) 0, (byte) 0);
|
||||
private static final int NORMAL_POS_Y = packNormal((byte) 0, (byte) 127, (byte) 0);
|
||||
private static final int NORMAL_NEG_Y = packNormal((byte) 0, (byte) -127, (byte) 0);
|
||||
private static final int NORMAL_POS_Z = packNormal((byte) 0, (byte) 0, (byte) 127);
|
||||
private static final int NORMAL_NEG_Z = packNormal((byte) 0, (byte) 0, (byte) -127);
|
||||
private static final int DIR_WEST = 0, DIR_EAST = 1, DIR_NORTH = 2, DIR_SOUTH = 3;
|
||||
private static final int DIR_NONE = 4;
|
||||
private static final int GROUP_PLATE_ALWAYS = 0, GROUP_PLATE_WEDGE = 1, GROUP_ALWAYS_DRAWN = 2, GROUP_WALL_WEDGE = 3;
|
||||
private static final int ORDER_GROUP_COUNT = 2 * WEDGE_COUNT + 3;
|
||||
private static final int ORDER_BANDS_PER_CELL = 4;
|
||||
private static final int WEDGE_BUCKETS = WEDGE_COUNT + 1;
|
||||
private static final int FACE_WEDGE_BUCKETS = 4 * WEDGE_BUCKETS;
|
||||
private static final int WALL_SORT_INDEX_BITS = 26;
|
||||
private static final int WALL_SORT_DISTANCE_BITS = 20;
|
||||
private static final int WALL_SORT_BUCKET_SHIFT = WALL_SORT_INDEX_BITS + WALL_SORT_DISTANCE_BITS;
|
||||
private static final long WALL_SORT_INDEX_MASK = (1L << WALL_SORT_INDEX_BITS) - 1;
|
||||
private static final int WALL_SORT_MAX_RUNS = 1 << WALL_SORT_INDEX_BITS;
|
||||
private static final int WALL_SORT_MAX_DISTANCE = (1 << WALL_SORT_DISTANCE_BITS) - 1;
|
||||
private static final int RADIX_BITS = 10;
|
||||
private static final int RADIX_SIZE = 1 << RADIX_BITS;
|
||||
private static final int RADIX_MASK = RADIX_SIZE - 1;
|
||||
private static final Tracy.ZoneId Z_STAMP_PLATES = Tracy.zoneId("cloudVertexStampPlates", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_PLATE_COVER = Tracy.zoneId("cloudPlateCover", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_STAMP_WALLS = Tracy.zoneId("cloudVertexStampWalls", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_UPLOAD = Tracy.zoneId("cloudVertexUpload", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_GEOMETRY = Tracy.zoneId("cloudVertexGeometry", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_REORDER = Tracy.zoneId("cloudVertexReorder", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_WALL_SORT = Tracy.zoneId("cloudVertexWallSort", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_REORDER_UPLOAD = Tracy.zoneId("cloudVertexReorderUpload", Tracy.COLOR_CLIENT);
|
||||
|
||||
static {
|
||||
FACE_SHADE_PACKED = new int[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
final int shadeByte = Math.round(FACE_SHADE[i] * 255f) & 0xFF;
|
||||
FACE_SHADE_PACKED[i] = BIG_ENDIAN ? shadeByte : (shadeByte << 24);
|
||||
}
|
||||
}
|
||||
|
||||
private final int[] faceWedgeStart = new int[FACE_WEDGE_BUCKETS + 1];
|
||||
private final int[] faceWedgeCursor = new int[FACE_WEDGE_BUCKETS];
|
||||
private final int[] wallStart = new int[WEDGE_COUNT + 1];
|
||||
private final int[] plateStart = new int[WEDGE_COUNT + 1];
|
||||
private final int[] wallStartScratch = new int[WEDGE_COUNT + 1];
|
||||
private final int[] plateStartScratch = new int[WEDGE_COUNT + 1];
|
||||
private int orderPendingQuads;
|
||||
private int orderPendingAnchorX, orderPendingAnchorZ;
|
||||
private final CloudPlateCover plates = new CloudPlateCover();
|
||||
private IVertexArrayObject vao;
|
||||
private int vertexCount;
|
||||
private int eboId = -1;
|
||||
private int eboQuadCapacity;
|
||||
private boolean eboIsSequential;
|
||||
private boolean vaoHasUv = true;
|
||||
private int vertexStride = STRIDE_WITH_UV;
|
||||
private int normalOffset = NORMAL_OFFSET_WITH_UV;
|
||||
private int quadBytes = 4 * STRIDE_WITH_UV;
|
||||
private ByteBuffer emitBuffer;
|
||||
private long emitBufferAddr;
|
||||
private int emitBufferCapacity;
|
||||
private long writeAddr, writeEnd;
|
||||
private ByteBuffer sortedBuffer;
|
||||
private long sortedBufferAddr;
|
||||
private int sortedBufferCapacity;
|
||||
private byte[] quadFace = new byte[2048];
|
||||
private int quadCount;
|
||||
private int[] quadCell = new int[2048];
|
||||
private short[] quadGroupBits = new short[2048];
|
||||
private int nextQuadCellX, nextQuadCellZ, nextQuadGroup, nextQuadWedge, nextQuadDir = DIR_NONE;
|
||||
private int buildAnchorX = Integer.MIN_VALUE, buildAnchorZ = Integer.MIN_VALUE;
|
||||
private int orderAnchorX = Integer.MIN_VALUE, orderAnchorZ = Integer.MIN_VALUE;
|
||||
private int buildRadiusCells;
|
||||
private int[] orderBucketCount = new int[0];
|
||||
private int[] quadBucket = new int[0];
|
||||
private IntBuffer orderIndexBuffer;
|
||||
private int[] orderIndexScratch = new int[0];
|
||||
private int[] quadSortKey = new int[0];
|
||||
private int[] wallRuns = new int[4096 * 4];
|
||||
private int wallRunCount;
|
||||
private int[] wallRunBucket = new int[4096];
|
||||
private int[] wallRunDistance = new int[4096];
|
||||
private long[] wallSortKey = new long[4096];
|
||||
private long[] wallSortScratch = new long[4096];
|
||||
private final int[] radixCounts = new int[RADIX_SIZE];
|
||||
private float textureScaleX, textureScaleZ;
|
||||
private boolean forPackShader;
|
||||
private int wallCutCells = Integer.MAX_VALUE;
|
||||
private int wallClipRadiusSq;
|
||||
private ByteBuffer pendingUpload;
|
||||
private int pendingVertices = -1;
|
||||
private int vboCapacityVertices;
|
||||
private int uploadCursorBytes = -1;
|
||||
private int uploadTotalBytes;
|
||||
private int uploadVertices;
|
||||
private static final Tracy.ZoneId Z_EBO = Tracy.zoneId("cloudVertexEbo", Tracy.COLOR_CLIENT);
|
||||
private static IntBuffer sharedSequentialIndices;
|
||||
private static int sharedSequentialQuads;
|
||||
|
||||
private static int packNormal(byte nx, byte ny, byte nz) {
|
||||
final int x = nx & 0xFF, y = ny & 0xFF, z = nz & 0xFF;
|
||||
return BIG_ENDIAN ? (x << 24) | (y << 16) | (z << 8) : x | (y << 8) | (z << 16);
|
||||
}
|
||||
|
||||
int quadCount() {
|
||||
return vertexCount / 4;
|
||||
}
|
||||
|
||||
boolean built() {
|
||||
return vao != null && vertexCount != 0;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
vertexCount = 0;
|
||||
orderAnchorX = Integer.MIN_VALUE;
|
||||
orderAnchorZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
int[] plateStarts() {
|
||||
return plateStart;
|
||||
}
|
||||
|
||||
int[] wallStarts() {
|
||||
return wallStart;
|
||||
}
|
||||
|
||||
void buildFast(CloudShape shape, int radiusCells, float scrollX, float scrollZ, boolean forPackShader) {
|
||||
beginBuild(shape, false, forPackShader);
|
||||
|
||||
nextQuadDir = DIR_NONE;
|
||||
nextQuadGroup = GROUP_PLATE_ALWAYS;
|
||||
nextQuadWedge = 0;
|
||||
nextQuadCellX = 0;
|
||||
nextQuadCellZ = 0;
|
||||
final double minCell = -radiusCells;
|
||||
final double maxCell = radiusCells;
|
||||
final float uWest = -radiusCells * textureScaleX + scrollX;
|
||||
final float uEast = radiusCells * textureScaleX + scrollX;
|
||||
final float vNorth = -radiusCells * textureScaleZ + scrollZ;
|
||||
final float vSouth = radiusCells * textureScaleZ + scrollZ;
|
||||
ensureEmitCapacity(quadBytes);
|
||||
emitQuad(minCell, 0.0, minCell, uWest, vNorth, maxCell, 0.0, minCell, uEast, vNorth,
|
||||
maxCell, 0.0, maxCell, uEast, vSouth, minCell, 0.0, maxCell, uWest, vSouth, FACE_UP, NORMAL_POS_Y);
|
||||
|
||||
finishGeometry();
|
||||
}
|
||||
|
||||
void build(CloudShape shape, int anchorX, int anchorZ, int radiusCells, int radiusCellsSq,
|
||||
double cellHeightBlocks, float scrollX, float scrollZ,
|
||||
boolean emitUnderside, boolean emitTopSurface, boolean emitPlates,
|
||||
int wallCut, int plateLodCells, boolean untextured, boolean forPack) {
|
||||
beginBuild(shape, untextured, forPack);
|
||||
this.wallCutCells = wallCut;
|
||||
buildAnchorX = anchorX;
|
||||
buildAnchorZ = anchorZ;
|
||||
buildRadiusCells = radiusCells;
|
||||
|
||||
final double deckTopY = cellHeightBlocks;
|
||||
final double plateTopY = deckTopY - EDGE_OVERLAP;
|
||||
|
||||
if (emitPlates) {
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_STAMP_PLATES);
|
||||
addPlates(shape, anchorX, anchorZ, radiusCells, radiusCellsSq, emitUnderside, emitTopSurface, plateTopY, scrollX, scrollZ, plateLodCells);
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_STAMP_WALLS);
|
||||
addWalls(shape, anchorX, anchorZ, radiusCells, radiusCellsSq, deckTopY, scrollX, scrollZ);
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_GEOMETRY);
|
||||
finishGeometry();
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
|
||||
void buildInterior(CloudShape shape, int anchorX, int anchorZ, double deckTopY,
|
||||
float scrollX, float scrollZ, boolean untextured, boolean forPack) {
|
||||
beginBuild(shape, untextured, forPack);
|
||||
addInteriorFaces(shape, anchorX, anchorZ, deckTopY, scrollX, scrollZ);
|
||||
finishGeometry();
|
||||
}
|
||||
|
||||
private void beginBuild(CloudShape shape, boolean untextured, boolean forPack) {
|
||||
textureScaleX = 1.0f / shape.width;
|
||||
textureScaleZ = 1.0f / shape.height;
|
||||
forPackShader = forPack;
|
||||
if (!forPack && quadCell.length < quadFace.length) {
|
||||
quadCell = new int[quadFace.length];
|
||||
quadGroupBits = new short[quadFace.length];
|
||||
}
|
||||
final boolean withUv = !untextured;
|
||||
vertexStride = withUv ? STRIDE_WITH_UV : STRIDE_NO_UV;
|
||||
normalOffset = withUv ? NORMAL_OFFSET_WITH_UV : NORMAL_OFFSET_NO_UV;
|
||||
quadBytes = 4 * vertexStride;
|
||||
prepareEmitBuffer();
|
||||
}
|
||||
|
||||
private void addPlates(CloudShape shape, int anchorX, int anchorZ, int radiusCells, int radiusCellsSq,
|
||||
boolean emitUnderside, boolean emitTopSurface, double plateTopY, float scrollX, float scrollZ, int plateLodCells) {
|
||||
final int bytesPerRect = (emitUnderside ? quadBytes : 0) + (emitTopSurface ? quadBytes : 0);
|
||||
if (bytesPerRect == 0) return;
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_PLATE_COVER);
|
||||
plates.build(shape, anchorX, anchorZ, radiusCells, radiusCellsSq, plateLodCells, !forPackShader);
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
|
||||
if (forPackShader) {
|
||||
nextQuadDir = DIR_NONE;
|
||||
nextQuadGroup = GROUP_PLATE_ALWAYS;
|
||||
nextQuadWedge = 0;
|
||||
if (emitUnderside) emitPlatePass(true, false, plateTopY, scrollX, scrollZ);
|
||||
if (emitTopSurface) emitPlatePass(false, true, plateTopY, scrollX, scrollZ);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < plates.count(); i++) {
|
||||
final int rect = plates.orderedRect(i);
|
||||
final int wedgeBucket = plates.wedgeBucket(rect);
|
||||
nextQuadDir = DIR_NONE;
|
||||
nextQuadGroup = wedgeBucket == 0 ? GROUP_PLATE_ALWAYS : GROUP_PLATE_WEDGE;
|
||||
nextQuadWedge = wedgeBucket == 0 ? 0 : wedgeBucket - 1;
|
||||
nextQuadCellX = plates.minX(rect);
|
||||
nextQuadCellZ = plates.minZ(rect);
|
||||
emitPlateRect(plates.minX(rect), plates.minZ(rect), plates.maxX(rect) + 1, plates.maxZ(rect) + 1,
|
||||
emitUnderside, emitTopSurface, plateTopY, scrollX, scrollZ, bytesPerRect);
|
||||
}
|
||||
}
|
||||
|
||||
private void emitPlatePass(boolean emitUnderside, boolean emitTopSurface, double plateTopY, float scrollX, float scrollZ) {
|
||||
for (int rect = 0; rect < plates.count(); rect++) {
|
||||
nextQuadCellX = plates.minX(rect);
|
||||
nextQuadCellZ = plates.minZ(rect);
|
||||
emitPlateRect(plates.minX(rect), plates.minZ(rect), plates.maxX(rect) + 1, plates.maxZ(rect) + 1,
|
||||
emitUnderside, emitTopSurface, plateTopY, scrollX, scrollZ, quadBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void addWalls(CloudShape shape, int anchorX, int anchorZ, int radiusCells, int radiusCellsSq,
|
||||
double deckTopY, float scrollX, float scrollZ) {
|
||||
final int texW = shape.width;
|
||||
final int texH = shape.height;
|
||||
|
||||
final int bandsPerWedge = radiusCells + 1;
|
||||
final long cutCellsSq = (long) wallCutCells * wallCutCells;
|
||||
wallClipRadiusSq = forPackShader && cutCellsSq < radiusCellsSq ? (int) cutCellsSq : radiusCellsSq;
|
||||
wallRunCount = 0;
|
||||
collectWallRuns(shape.westRuns, DIR_WEST, anchorX, anchorZ, texW, texH, bandsPerWedge);
|
||||
collectWallRuns(shape.eastRuns, DIR_EAST, anchorX, anchorZ, texW, texH, bandsPerWedge);
|
||||
collectWallRuns(shape.northRuns, DIR_NORTH, anchorX, anchorZ, texW, texH, bandsPerWedge);
|
||||
collectWallRuns(shape.southRuns, DIR_SOUTH, anchorX, anchorZ, texW, texH, bandsPerWedge);
|
||||
final int found = wallRunCount;
|
||||
|
||||
if (found == 0) return;
|
||||
|
||||
if (forPackShader) {
|
||||
nextQuadGroup = GROUP_ALWAYS_DRAWN;
|
||||
nextQuadWedge = 0;
|
||||
for (int i = 0; i < found; i++) {
|
||||
final int run = i * 4;
|
||||
nextQuadDir = wallRuns[run];
|
||||
nextQuadCellX = wallRuns[run + 1];
|
||||
nextQuadCellZ = wallRuns[run + 2];
|
||||
emitWallRun(wallRuns[run], wallRuns[run + 1], wallRuns[run + 2], wallRuns[run + 3], deckTopY, scrollX, scrollZ);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (wallSortKey.length < found) wallSortKey = new long[found];
|
||||
for (int i = 0; i < found; i++) {
|
||||
wallSortKey[i] = ((long) wallRunBucket[i] << WALL_SORT_BUCKET_SHIFT)
|
||||
| ((long) wallRunDistance[i] << WALL_SORT_INDEX_BITS)
|
||||
| i;
|
||||
}
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_WALL_SORT);
|
||||
radixSortWallKeys(found);
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
|
||||
for (int i = 0; i < found; i++) {
|
||||
final long key = wallSortKey[i];
|
||||
final int bucket = (int) (key >>> WALL_SORT_BUCKET_SHIFT);
|
||||
final int wedge = bucket == 0 ? -1 : (bucket - 1) / bandsPerWedge;
|
||||
final int run = (int) (key & WALL_SORT_INDEX_MASK) * 4;
|
||||
nextQuadGroup = bucket == 0 ? GROUP_ALWAYS_DRAWN : GROUP_WALL_WEDGE;
|
||||
nextQuadWedge = bucket == 0 ? 0 : wedge;
|
||||
nextQuadDir = wallRuns[run];
|
||||
nextQuadCellX = wallRuns[run + 1];
|
||||
nextQuadCellZ = wallRuns[run + 2];
|
||||
emitWallRun(wallRuns[run], wallRuns[run + 1], wallRuns[run + 2], wallRuns[run + 3], deckTopY, scrollX, scrollZ);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectWallRuns(int[] runs, int dir, int anchorX, int anchorZ, int texW, int texH, int bandsPerWedge) {
|
||||
final boolean alongZ = dir == DIR_WEST || dir == DIR_EAST;
|
||||
final int clipRadiusCells = (int) Math.sqrt(wallClipRadiusSq);
|
||||
final int minCell = -clipRadiusCells;
|
||||
final int maxCell = clipRadiusCells;
|
||||
|
||||
for (int r = 0; r < runs.length; r += 3) {
|
||||
final int len = runs[r + 2];
|
||||
final int baseX = Math.floorMod(runs[r] - anchorX, texW);
|
||||
final int baseZ = Math.floorMod(runs[r + 1] - anchorZ, texH);
|
||||
|
||||
for (int kz = Math.floorDiv(minCell - baseZ - (alongZ ? len - 1 : 0), texH); kz <= Math.floorDiv(maxCell - baseZ, texH); kz++) {
|
||||
final int runZ = baseZ + kz * texH;
|
||||
for (int kx = Math.floorDiv(minCell - baseX - (alongZ ? 0 : len - 1), texW); kx <= Math.floorDiv(maxCell - baseX, texW); kx++) {
|
||||
final int runX = baseX + kx * texW;
|
||||
|
||||
final int startX, startZ, span;
|
||||
if (alongZ) {
|
||||
if (runX * runX > wallClipRadiusSq) continue;
|
||||
final int halfSpan = (int) Math.sqrt(wallClipRadiusSq - runX * runX);
|
||||
final int z0 = Math.max(runZ, -halfSpan);
|
||||
final int z1 = Math.min(runZ + len - 1, halfSpan);
|
||||
if (z0 > z1) continue;
|
||||
startX = runX;
|
||||
startZ = z0;
|
||||
span = z1 - z0 + 1;
|
||||
} else {
|
||||
if (runZ * runZ > wallClipRadiusSq) continue;
|
||||
final int halfSpan = (int) Math.sqrt(wallClipRadiusSq - runZ * runZ);
|
||||
final int x0 = Math.max(runX, -halfSpan);
|
||||
final int x1 = Math.min(runX + len - 1, halfSpan);
|
||||
if (x0 > x1) continue;
|
||||
startX = x0;
|
||||
startZ = runZ;
|
||||
span = x1 - x0 + 1;
|
||||
}
|
||||
|
||||
if (forPackShader) {
|
||||
recordWallRun(dir, startX, startZ, span, 0, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
int pieceStart = 0;
|
||||
int pieceWedge = wedgeOf(startX, startZ);
|
||||
double pieceDist = Math.sqrt(startX * startX + startZ * startZ);
|
||||
int pieceBand = (int) pieceDist;
|
||||
for (int c = 1; c <= span; c++) {
|
||||
final int cX = alongZ ? startX : startX + c;
|
||||
final int cZ = alongZ ? startZ + c : startZ;
|
||||
final boolean last = c == span;
|
||||
final int wedgeHere = last ? -1 : wedgeOf(cX, cZ);
|
||||
final double distHere = last ? -1.0 : Math.sqrt(cX * cX + cZ * cZ);
|
||||
final int bandHere = last ? -1 : (int) distHere;
|
||||
if (wedgeHere == pieceWedge && bandHere == pieceBand) {
|
||||
if (distHere < pieceDist) pieceDist = distHere;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pieceBand <= wallCutCells) {
|
||||
recordWallRun(dir,
|
||||
alongZ ? startX : startX + pieceStart,
|
||||
alongZ ? startZ + pieceStart : startZ,
|
||||
c - pieceStart,
|
||||
pieceBand <= ALWAYS_DRAWN_CELLS ? 0 : 1 + pieceWedge * bandsPerWedge + pieceBand,
|
||||
(int) (pieceDist * 256.0));
|
||||
}
|
||||
|
||||
pieceStart = c;
|
||||
pieceWedge = wedgeHere;
|
||||
pieceDist = distHere;
|
||||
pieceBand = bandHere;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void radixSortWallKeys(int count) {
|
||||
if (count < 2) return;
|
||||
if (wallSortScratch.length < count) wallSortScratch = new long[count];
|
||||
|
||||
long[] src = wallSortKey;
|
||||
long[] dst = wallSortScratch;
|
||||
final int[] counts = radixCounts;
|
||||
|
||||
for (int shift = WALL_SORT_INDEX_BITS; shift < Long.SIZE; shift += RADIX_BITS) {
|
||||
Arrays.fill(counts, 0);
|
||||
for (int i = 0; i < count; i++) counts[(int) ((src[i] >>> shift) & RADIX_MASK)]++;
|
||||
|
||||
boolean uniform = false;
|
||||
int acc = 0;
|
||||
for (int d = 0; d < RADIX_SIZE; d++) {
|
||||
final int c = counts[d];
|
||||
if (c == count) uniform = true;
|
||||
counts[d] = acc;
|
||||
acc += c;
|
||||
}
|
||||
if (uniform) continue;
|
||||
|
||||
for (int i = 0; i < count; i++) dst[counts[(int) ((src[i] >>> shift) & RADIX_MASK)]++] = src[i];
|
||||
final long[] swap = src;
|
||||
src = dst;
|
||||
dst = swap;
|
||||
}
|
||||
|
||||
if (src != wallSortKey) System.arraycopy(src, 0, wallSortKey, 0, count);
|
||||
}
|
||||
|
||||
private void recordWallRun(int dir, int relX, int relZ, int span, int bucket, int distance256ths) {
|
||||
if (wallRunCount == WALL_SORT_MAX_RUNS) return;
|
||||
if (wallRunCount * 4 + 4 > wallRuns.length) {
|
||||
wallRuns = Arrays.copyOf(wallRuns, wallRuns.length * 2);
|
||||
wallRunBucket = Arrays.copyOf(wallRunBucket, wallRunBucket.length * 2);
|
||||
wallRunDistance = Arrays.copyOf(wallRunDistance, wallRunDistance.length * 2);
|
||||
}
|
||||
final int slot = wallRunCount * 4;
|
||||
wallRuns[slot] = dir;
|
||||
wallRuns[slot + 1] = relX;
|
||||
wallRuns[slot + 2] = relZ;
|
||||
wallRuns[slot + 3] = span;
|
||||
wallRunBucket[wallRunCount] = bucket;
|
||||
wallRunDistance[wallRunCount] = Math.min(distance256ths, WALL_SORT_MAX_DISTANCE);
|
||||
wallRunCount++;
|
||||
}
|
||||
|
||||
private void emitWallRun(int dir, int relX, int relZ, int span, double deckTopY, float scrollX, float scrollZ) {
|
||||
ensureEmitCapacity(quadBytes);
|
||||
final double x0 = relX;
|
||||
final double z0 = relZ;
|
||||
|
||||
if (dir == DIR_WEST || dir == DIR_EAST) {
|
||||
final double z1 = relZ + span;
|
||||
final float uCentre = (relX + 0.5F) * textureScaleX + scrollX;
|
||||
final float vNorth = relZ * textureScaleZ + scrollZ;
|
||||
final float vSouth = (relZ + span) * textureScaleZ + scrollZ;
|
||||
if (dir == DIR_WEST) {
|
||||
emitQuad(x0, 0.0, z1, uCentre, vSouth, x0, deckTopY, z1, uCentre, vSouth,
|
||||
x0, deckTopY, z0, uCentre, vNorth, x0, 0.0, z0, uCentre, vNorth, FACE_SIDE_X, NORMAL_NEG_X);
|
||||
} else {
|
||||
final double xEast = relX + 1 - EDGE_OVERLAP;
|
||||
emitQuad(xEast, 0.0, z0, uCentre, vNorth, xEast, deckTopY, z0, uCentre, vNorth,
|
||||
xEast, deckTopY, z1, uCentre, vSouth, xEast, 0.0, z1, uCentre, vSouth, FACE_SIDE_X, NORMAL_POS_X);
|
||||
}
|
||||
} else {
|
||||
final double x1 = relX + span;
|
||||
final float uLeft = relX * textureScaleX + scrollX;
|
||||
final float uRight = (relX + span) * textureScaleX + scrollX;
|
||||
final float vCentre = (relZ + 0.5F) * textureScaleZ + scrollZ;
|
||||
if (dir == DIR_NORTH) {
|
||||
emitQuad(x0, deckTopY, z0, uLeft, vCentre, x1, deckTopY, z0, uRight, vCentre,
|
||||
x1, 0.0, z0, uRight, vCentre, x0, 0.0, z0, uLeft, vCentre, FACE_SIDE_Z, NORMAL_NEG_Z);
|
||||
} else {
|
||||
final double zSouth = relZ + 1 - EDGE_OVERLAP;
|
||||
emitQuad(x0, 0.0, zSouth, uLeft, vCentre, x1, 0.0, zSouth, uRight, vCentre,
|
||||
x1, deckTopY, zSouth, uRight, vCentre, x0, deckTopY, zSouth, uLeft, vCentre, FACE_SIDE_Z, NORMAL_POS_Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void emitPlateRect(int relX0, int relZ0, int relX1, int relZ1, boolean emitUnderside, boolean emitTopSurface,
|
||||
double plateTopY, float scrollX, float scrollZ, int bytesPerRect) {
|
||||
final double x0 = relX0, x1 = relX1, z0 = relZ0, z1 = relZ1;
|
||||
final float uLeft = relX0 * textureScaleX + scrollX;
|
||||
final float uRight = relX1 * textureScaleX + scrollX;
|
||||
final float vNorth = relZ0 * textureScaleZ + scrollZ;
|
||||
final float vSouth = relZ1 * textureScaleZ + scrollZ;
|
||||
|
||||
ensureEmitCapacity(bytesPerRect);
|
||||
if (emitUnderside) {
|
||||
emitQuad(x0, 0.0, z0, uLeft, vNorth, x1, 0.0, z0, uRight, vNorth,
|
||||
x1, 0.0, z1, uRight, vSouth, x0, 0.0, z1, uLeft, vSouth, FACE_DOWN, NORMAL_NEG_Y);
|
||||
}
|
||||
if (emitTopSurface) {
|
||||
emitQuad(x0, plateTopY, z1, uLeft, vSouth, x1, plateTopY, z1, uRight, vSouth,
|
||||
x1, plateTopY, z0, uRight, vNorth, x0, plateTopY, z0, uLeft, vNorth, FACE_UP, NORMAL_POS_Y);
|
||||
}
|
||||
}
|
||||
|
||||
private void addInteriorFaces(CloudShape shape, int anchorX, int anchorZ, double deckTopY, float scrollX, float scrollZ) {
|
||||
if ((shape.cellFlagsAt(anchorX, anchorZ) & CloudShape.OPAQUE) == 0) return;
|
||||
|
||||
nextQuadDir = DIR_NONE;
|
||||
nextQuadGroup = GROUP_ALWAYS_DRAWN;
|
||||
nextQuadWedge = 0;
|
||||
nextQuadCellX = 0;
|
||||
nextQuadCellZ = 0;
|
||||
|
||||
final float uLeft = scrollX;
|
||||
final float uRight = textureScaleX + scrollX;
|
||||
final float uCentre = 0.5F * textureScaleX + scrollX;
|
||||
final float vNorth = scrollZ;
|
||||
final float vSouth = textureScaleZ + scrollZ;
|
||||
final float vCentre = 0.5F * textureScaleZ + scrollZ;
|
||||
|
||||
final double x0 = 0.0;
|
||||
final double x1 = 1.0;
|
||||
final double z0 = 0.0;
|
||||
final double z1 = 1.0;
|
||||
final double yBottom = 0.0;
|
||||
final double yTop = deckTopY;
|
||||
|
||||
ensureEmitCapacity(6 * quadBytes);
|
||||
emitQuad(x0, yBottom, z1, uLeft, vSouth, x1, yBottom, z1, uRight, vSouth, x1, yBottom, z0, uRight, vNorth, x0, yBottom, z0, uLeft, vNorth, FACE_DOWN, NORMAL_POS_Y);
|
||||
emitQuad(x0, yTop, z0, uLeft, vNorth, x1, yTop, z0, uRight, vNorth, x1, yTop, z1, uRight, vSouth, x0, yTop, z1, uLeft, vSouth, FACE_UP, NORMAL_NEG_Y);
|
||||
emitQuad(x0, yBottom, z0, uCentre, vNorth, x0, yTop, z0, uCentre, vNorth, x0, yTop, z1, uCentre, vSouth, x0, yBottom, z1, uCentre, vSouth, FACE_SIDE_X, NORMAL_POS_X);
|
||||
emitQuad(x1, yBottom, z1, uCentre, vSouth, x1, yTop, z1, uCentre, vSouth, x1, yTop, z0, uCentre, vNorth, x1, yBottom, z0, uCentre, vNorth, FACE_SIDE_X, NORMAL_NEG_X);
|
||||
emitQuad(x0, yBottom, z0, uLeft, vCentre, x1, yBottom, z0, uRight, vCentre, x1, yTop, z0, uRight, vCentre, x0, yTop, z0, uLeft, vCentre, FACE_SIDE_Z, NORMAL_POS_Z);
|
||||
emitQuad(x0, yTop, z1, uLeft, vCentre, x1, yTop, z1, uRight, vCentre, x1, yBottom, z1, uRight, vCentre, x0, yBottom, z1, uLeft, vCentre, FACE_SIDE_Z, NORMAL_NEG_Z);
|
||||
}
|
||||
|
||||
private void finishGeometry() {
|
||||
final int totalBytes = (int) (writeAddr - emitBufferAddr);
|
||||
final int vertices = totalBytes / vertexStride;
|
||||
pendingVertices = vertices;
|
||||
if (vertices == 0) {
|
||||
pendingUpload = null;
|
||||
return;
|
||||
}
|
||||
if (forPackShader) {
|
||||
pendingUpload = sortQuadsByFace(totalBytes);
|
||||
} else {
|
||||
emitBuffer.position(0).limit(totalBytes);
|
||||
pendingUpload = emitBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
void discardPendingUpload() {
|
||||
pendingVertices = -1;
|
||||
pendingUpload = null;
|
||||
uploadCursorBytes = -1;
|
||||
}
|
||||
|
||||
void uploadBuilt() {
|
||||
if (beginUpload()) uploadSlice(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
boolean beginUpload() {
|
||||
final int vertices = pendingVertices;
|
||||
pendingVertices = -1;
|
||||
if (vertices <= 0) {
|
||||
pendingUpload = null;
|
||||
if (vao != null) {
|
||||
vao.delete();
|
||||
vao = null;
|
||||
}
|
||||
vboCapacityVertices = 0;
|
||||
vertexCount = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_UPLOAD);
|
||||
try {
|
||||
final boolean wantUv = vertexStride == STRIDE_WITH_UV;
|
||||
if (vao != null && vaoHasUv != wantUv) {
|
||||
vao.delete();
|
||||
vao = null;
|
||||
vboCapacityVertices = 0;
|
||||
}
|
||||
|
||||
uploadVertices = vertices;
|
||||
uploadTotalBytes = vertices * vertexStride;
|
||||
|
||||
if (vao != null && vertices <= vboCapacityVertices) {
|
||||
uploadCursorBytes = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
final ByteBuffer source = pendingUpload;
|
||||
final int roomyVertices = clamp(vertices, vertices + (vertices >> 1), source.capacity() / vertexStride);
|
||||
source.position(0).limit(roomyVertices * vertexStride);
|
||||
if (vao == null) {
|
||||
vao = VertexBufferType.MUTABLE_RESIZABLE.allocate(
|
||||
wantUv ? DefaultVertexFormat.POSITION_TEXTURE_NORMAL : DefaultVertexFormat.POSITION_NORMAL,
|
||||
GL11.GL_TRIANGLES, source, roomyVertices);
|
||||
vaoHasUv = wantUv;
|
||||
setupShadeAttrib();
|
||||
} else {
|
||||
vao.getVBO().allocate(source, roomyVertices);
|
||||
}
|
||||
vboCapacityVertices = roomyVertices;
|
||||
uploadCursorBytes = uploadTotalBytes;
|
||||
return true;
|
||||
} finally {
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
}
|
||||
|
||||
boolean uploadSlice(int maxBytes) {
|
||||
if (uploadCursorBytes < 0) return true;
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_UPLOAD);
|
||||
try {
|
||||
if (uploadCursorBytes < uploadTotalBytes) {
|
||||
final int sliceBytes = Math.min(maxBytes, uploadTotalBytes - uploadCursorBytes);
|
||||
final ByteBuffer source = pendingUpload;
|
||||
source.position(uploadCursorBytes).limit(uploadCursorBytes + sliceBytes);
|
||||
vao.getVBO().update(source, uploadCursorBytes);
|
||||
uploadCursorBytes += sliceBytes;
|
||||
if (uploadCursorBytes < uploadTotalBytes) return false;
|
||||
}
|
||||
finishUpload();
|
||||
return true;
|
||||
} finally {
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
}
|
||||
|
||||
private void finishUpload() {
|
||||
uploadCursorBytes = -1;
|
||||
pendingUpload = null;
|
||||
vertexCount = uploadVertices;
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_EBO);
|
||||
ensureEbo(uploadVertices / 4);
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
orderAnchorX = Integer.MIN_VALUE;
|
||||
orderAnchorZ = Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
private void setupShadeAttrib() {
|
||||
vao.bind();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vao.getVBO().getId());
|
||||
GLStateManager.glEnableVertexAttribArray(FACE_SHADE_ATTRIB);
|
||||
GLStateManager.glVertexAttribPointer(FACE_SHADE_ATTRIB, 1, GL11.GL_UNSIGNED_BYTE, true, vertexStride, normalOffset + 3);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
vao.unbind();
|
||||
}
|
||||
|
||||
boolean orderStale(int anchorX, int anchorZ) {
|
||||
return anchorX != orderAnchorX || anchorZ != orderAnchorZ;
|
||||
}
|
||||
|
||||
boolean orderPublished() {
|
||||
return orderAnchorX != Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
void reorder(int anchorX, int anchorZ) {
|
||||
final int quads = vertexCount / 4;
|
||||
if (quads == 0) return;
|
||||
if (quads > quadCell.length) return;
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_REORDER);
|
||||
|
||||
final int dx = anchorX - buildAnchorX;
|
||||
final int dz = anchorZ - buildAnchorZ;
|
||||
final int bands = (buildRadiusCells + 2) * ORDER_BANDS_PER_CELL;
|
||||
final int bucketCount = ORDER_GROUP_COUNT * bands;
|
||||
|
||||
if (orderBucketCount.length < bucketCount + 1) orderBucketCount = new int[bucketCount + 1];
|
||||
else Arrays.fill(orderBucketCount, 0, bucketCount + 1, 0);
|
||||
|
||||
if (quadBucket.length < quads) quadBucket = new int[quads];
|
||||
|
||||
for (int q = 0; q < quads; q++) {
|
||||
final int cell = quadCell[q];
|
||||
final int cx = (short) (cell >> 16) - dx;
|
||||
final int cz = (short) cell - dz;
|
||||
final int groupBits = quadGroupBits[q];
|
||||
final int group = (groupBits >> 5) & 3;
|
||||
final int wedge = groupBits & 31;
|
||||
|
||||
final boolean facingAway = group == GROUP_WALL_WEDGE && switch ((groupBits >> 7) & 7) {
|
||||
case DIR_WEST -> cx < 1;
|
||||
case DIR_EAST -> cx > -1;
|
||||
case DIR_NORTH -> cz < 1;
|
||||
case DIR_SOUTH -> cz > -1;
|
||||
default -> false;
|
||||
};
|
||||
|
||||
int band = (int) (Math.sqrt((double) cx * cx + (double) cz * cz) * ORDER_BANDS_PER_CELL);
|
||||
if (band >= bands) band = bands - 1;
|
||||
|
||||
final int groupSlot = facingAway ? 2 + 2 * WEDGE_COUNT : switch (group) {
|
||||
case GROUP_PLATE_ALWAYS -> 0;
|
||||
case GROUP_PLATE_WEDGE -> 1 + wedge;
|
||||
case GROUP_ALWAYS_DRAWN -> 1 + WEDGE_COUNT;
|
||||
default -> 2 + WEDGE_COUNT + wedge;
|
||||
};
|
||||
final int bucket = groupSlot * bands + band;
|
||||
quadBucket[q] = bucket;
|
||||
orderBucketCount[bucket]++;
|
||||
}
|
||||
|
||||
int acc = 0;
|
||||
for (int b = 0; b < bucketCount; b++) {
|
||||
final int count = orderBucketCount[b];
|
||||
orderBucketCount[b] = acc;
|
||||
acc += count;
|
||||
}
|
||||
orderBucketCount[bucketCount] = acc;
|
||||
|
||||
for (int w = 0; w < WEDGE_COUNT; w++) plateStartScratch[w] = orderBucketCount[(1 + w) * bands];
|
||||
plateStartScratch[WEDGE_COUNT] = orderBucketCount[(1 + WEDGE_COUNT) * bands];
|
||||
for (int w = 0; w < WEDGE_COUNT; w++) wallStartScratch[w] = orderBucketCount[(2 + WEDGE_COUNT + w) * bands];
|
||||
wallStartScratch[WEDGE_COUNT] = orderBucketCount[(2 + 2 * WEDGE_COUNT) * bands];
|
||||
|
||||
final int needed = quads * 6;
|
||||
if (orderIndexBuffer == null || orderIndexBuffer.capacity() < needed) {
|
||||
orderIndexBuffer = BufferUtils.createIntBuffer(Math.max(needed, 4096));
|
||||
}
|
||||
|
||||
if (orderIndexScratch.length < needed) orderIndexScratch = new int[needed];
|
||||
final int[] out = orderIndexScratch;
|
||||
for (int q = 0; q < quads; q++) {
|
||||
final int slot = orderBucketCount[quadBucket[q]]++;
|
||||
final int v = q * 4;
|
||||
final int base = slot * 6;
|
||||
out[base] = v;
|
||||
out[base + 1] = v + 1;
|
||||
out[base + 2] = v + 2;
|
||||
out[base + 3] = v;
|
||||
out[base + 4] = v + 2;
|
||||
out[base + 5] = v + 3;
|
||||
}
|
||||
|
||||
final IntBuffer indices = orderIndexBuffer;
|
||||
indices.clear();
|
||||
indices.put(out, 0, needed);
|
||||
indices.flip();
|
||||
|
||||
orderPendingQuads = quads;
|
||||
orderPendingAnchorX = anchorX;
|
||||
orderPendingAnchorZ = anchorZ;
|
||||
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
|
||||
void reorderUpload() {
|
||||
if (orderPendingQuads <= 0) return;
|
||||
if (Tracy.FINE_ZONES) Tracy.beginZone(Z_REORDER_UPLOAD);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
|
||||
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, orderIndexBuffer, GL15.GL_DYNAMIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
System.arraycopy(plateStartScratch, 0, plateStart, 0, plateStart.length);
|
||||
System.arraycopy(wallStartScratch, 0, wallStart, 0, wallStart.length);
|
||||
eboQuadCapacity = orderPendingQuads;
|
||||
eboIsSequential = false;
|
||||
orderAnchorX = orderPendingAnchorX;
|
||||
orderAnchorZ = orderPendingAnchorZ;
|
||||
orderPendingQuads = 0;
|
||||
if (Tracy.FINE_ZONES) Tracy.endZone();
|
||||
}
|
||||
|
||||
void discardPendingReorder() {
|
||||
orderPendingQuads = 0;
|
||||
}
|
||||
|
||||
private void ensureEbo(int quads) {
|
||||
if (eboId == -1) {
|
||||
eboId = GLStateManager.glGenBuffers();
|
||||
}
|
||||
|
||||
if (quads <= eboQuadCapacity && eboIsSequential) return;
|
||||
eboIsSequential = true;
|
||||
final int capacity = Math.max(quads + (quads >> 1), 4096);
|
||||
final IntBuffer indices = sequentialIndices(capacity);
|
||||
indices.position(0).limit(capacity * 6);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
|
||||
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indices, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
eboQuadCapacity = capacity;
|
||||
}
|
||||
|
||||
private static IntBuffer sequentialIndices(int quads) {
|
||||
if (sharedSequentialIndices != null && sharedSequentialQuads >= quads) return sharedSequentialIndices;
|
||||
|
||||
final IntBuffer indices = BufferUtils.createIntBuffer(quads * 6);
|
||||
for (int q = 0; q < quads; q++) {
|
||||
final int v = q * 4;
|
||||
indices.put(v).put(v + 1).put(v + 2);
|
||||
indices.put(v).put(v + 2).put(v + 3);
|
||||
}
|
||||
indices.flip();
|
||||
sharedSequentialIndices = indices;
|
||||
sharedSequentialQuads = quads;
|
||||
return indices;
|
||||
}
|
||||
|
||||
void deleteEbo() {
|
||||
if (eboId != -1) {
|
||||
GLStateManager.glDeleteBuffers(eboId);
|
||||
eboId = -1;
|
||||
eboQuadCapacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void deleteVao() {
|
||||
if (vao != null) {
|
||||
vao.delete();
|
||||
vao = null;
|
||||
}
|
||||
vboCapacityVertices = 0;
|
||||
vertexCount = 0;
|
||||
}
|
||||
|
||||
void bind() {
|
||||
vao.bind();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
vao.unbind();
|
||||
}
|
||||
|
||||
void drawRange(int startQuad, int endQuad) {
|
||||
final int quads = endQuad - startQuad;
|
||||
if (quads <= 0) return;
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, quads * 6, GL11.GL_UNSIGNED_INT, (long) startQuad * 6L * 4L);
|
||||
}
|
||||
|
||||
void drawAll() {
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, (vertexCount / 4) * 6, GL11.GL_UNSIGNED_INT, 0L);
|
||||
}
|
||||
|
||||
boolean faceEmpty(int face) {
|
||||
final int base = face * WEDGE_BUCKETS;
|
||||
return faceWedgeStart[base + WEDGE_BUCKETS] == faceWedgeStart[base];
|
||||
}
|
||||
|
||||
void drawFace(int face) {
|
||||
final int base = face * WEDGE_BUCKETS;
|
||||
drawRange(faceWedgeStart[base], faceWedgeStart[base + WEDGE_BUCKETS]);
|
||||
}
|
||||
|
||||
private void prepareEmitBuffer() {
|
||||
if (emitBuffer == null) {
|
||||
emitBufferCapacity = 256 * 1024;
|
||||
emitBuffer = MemoryUtilities.memRealloc((ByteBuffer) null, emitBufferCapacity);
|
||||
emitBufferAddr = MemoryUtilities.memAddress(emitBuffer, 0);
|
||||
}
|
||||
|
||||
emitBuffer.clear();
|
||||
writeAddr = emitBufferAddr;
|
||||
writeEnd = emitBufferAddr + emitBufferCapacity;
|
||||
quadCount = 0;
|
||||
}
|
||||
|
||||
private void growEmitBuffer(int neededBytes) {
|
||||
final long oldBytes = writeAddr - emitBufferAddr;
|
||||
int newCap = emitBufferCapacity;
|
||||
while (newCap < neededBytes) newCap *= 2;
|
||||
emitBuffer = MemoryUtilities.memRealloc(emitBuffer, newCap);
|
||||
emitBufferAddr = MemoryUtilities.memAddress(emitBuffer, 0);
|
||||
emitBufferCapacity = newCap;
|
||||
writeAddr = emitBufferAddr + oldBytes;
|
||||
writeEnd = emitBufferAddr + newCap;
|
||||
}
|
||||
|
||||
private void ensureEmitCapacity(int bytes) {
|
||||
if (writeAddr + bytes > writeEnd) {
|
||||
growEmitBuffer((int) (writeAddr - emitBufferAddr) + bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private void emitQuad(double x0, double y0, double z0, float u0, float v0, double x1, double y1, double z1, float u1, float v1, double x2, double y2, double z2, float u2, float v2, double x3, double y3, double z3, float u3, float v3, byte face, int packedNormal) {
|
||||
if (quadCount == quadFace.length) {
|
||||
quadFace = Arrays.copyOf(quadFace, quadFace.length * 2);
|
||||
if (!forPackShader) {
|
||||
quadCell = Arrays.copyOf(quadCell, quadCell.length * 2);
|
||||
quadGroupBits = Arrays.copyOf(quadGroupBits, quadGroupBits.length * 2);
|
||||
}
|
||||
}
|
||||
if (!forPackShader) {
|
||||
quadCell[quadCount] = (nextQuadCellX << 16) | (nextQuadCellZ & 0xFFFF);
|
||||
quadGroupBits[quadCount] = (short) ((nextQuadDir << 7) | (nextQuadGroup << 5) | nextQuadWedge);
|
||||
}
|
||||
quadFace[quadCount++] = face;
|
||||
|
||||
final int normalAndShade = packedNormal | FACE_SHADE_PACKED[face];
|
||||
long addr = writeAddr;
|
||||
addr = writeVertex(addr, x0, y0, z0, u0, v0, normalAndShade);
|
||||
addr = writeVertex(addr, x1, y1, z1, u1, v1, normalAndShade);
|
||||
addr = writeVertex(addr, x2, y2, z2, u2, v2, normalAndShade);
|
||||
addr = writeVertex(addr, x3, y3, z3, u3, v3, normalAndShade);
|
||||
writeAddr = addr;
|
||||
}
|
||||
|
||||
private long writeVertex(long addr, double x, double y, double z, float u, float v, int normalAndShade) {
|
||||
MemoryUtilities.memPutFloat(addr, (float) x);
|
||||
MemoryUtilities.memPutFloat(addr + 4, (float) y);
|
||||
MemoryUtilities.memPutFloat(addr + 8, (float) z);
|
||||
if (normalOffset == NORMAL_OFFSET_WITH_UV) {
|
||||
MemoryUtilities.memPutFloat(addr + 12, u);
|
||||
MemoryUtilities.memPutFloat(addr + 16, v);
|
||||
}
|
||||
MemoryUtilities.memPutInt(addr + normalOffset, normalAndShade);
|
||||
return addr + vertexStride;
|
||||
}
|
||||
|
||||
private ByteBuffer sortQuadsByFace(int emittedBytes) {
|
||||
final int quadTotal = quadCount;
|
||||
Arrays.fill(faceWedgeStart, 0);
|
||||
|
||||
if (quadSortKey.length < quadTotal) quadSortKey = new int[quadTotal];
|
||||
final byte[] faces = quadFace;
|
||||
boolean alreadyGrouped = true;
|
||||
int previousKey = 0;
|
||||
for (int q = 0; q < quadTotal; q++) {
|
||||
final int key = faces[q] * WEDGE_BUCKETS;
|
||||
if (key < previousKey) alreadyGrouped = false;
|
||||
previousKey = key;
|
||||
quadSortKey[q] = key;
|
||||
faceWedgeStart[key]++;
|
||||
}
|
||||
|
||||
int acc = 0;
|
||||
for (int b = 0; b < FACE_WEDGE_BUCKETS; b++) {
|
||||
final int count = faceWedgeStart[b];
|
||||
faceWedgeStart[b] = acc;
|
||||
faceWedgeCursor[b] = acc;
|
||||
acc += count;
|
||||
}
|
||||
faceWedgeStart[FACE_WEDGE_BUCKETS] = acc;
|
||||
|
||||
if (alreadyGrouped) {
|
||||
emitBuffer.position(0).limit(emittedBytes);
|
||||
return emitBuffer;
|
||||
}
|
||||
|
||||
final int totalBytes = quadTotal * quadBytes;
|
||||
if (sortedBuffer == null || sortedBufferCapacity < totalBytes) {
|
||||
int rounded = Math.max(totalBytes, 64 * 1024);
|
||||
int pow2 = Integer.highestOneBit(rounded - 1) << 1;
|
||||
if (pow2 <= 0) pow2 = rounded;
|
||||
sortedBuffer = MemoryUtilities.memRealloc(sortedBuffer, pow2);
|
||||
sortedBufferAddr = MemoryUtilities.memAddress(sortedBuffer, 0);
|
||||
sortedBufferCapacity = pow2;
|
||||
}
|
||||
|
||||
final long srcBase = emitBufferAddr;
|
||||
int q = 0;
|
||||
while (q < quadTotal) {
|
||||
final int key = quadSortKey[q];
|
||||
int runEnd = q + 1;
|
||||
while (runEnd < quadTotal && quadSortKey[runEnd] == key) runEnd++;
|
||||
final int runLen = runEnd - q;
|
||||
final long runBytes = (long) runLen * quadBytes;
|
||||
MemoryUtilities.memCopy(srcBase + (long) q * quadBytes,
|
||||
sortedBufferAddr + (long) faceWedgeCursor[key] * quadBytes, runBytes);
|
||||
faceWedgeCursor[key] += runLen;
|
||||
q = runEnd;
|
||||
}
|
||||
|
||||
sortedBuffer.position(0).limit(totalBytes);
|
||||
return sortedBuffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ArmorTexturePaths {
|
||||
private static final int STRIDE = 5;
|
||||
private static String[] table = new String[0];
|
||||
|
||||
public static String path(String prefix, int renderIndex, int slot, String type) {
|
||||
if (prefix == null || (type != null && !"overlay".equals(type))) return format(prefix, slot, type);
|
||||
String[] t = table;
|
||||
final int base = renderIndex * STRIDE;
|
||||
if (base >= t.length) {
|
||||
t = Arrays.copyOf(t, base + STRIDE);
|
||||
table = t;
|
||||
}
|
||||
if (t[base] != prefix) {
|
||||
t[base + 1] = null;
|
||||
t[base + 2] = null;
|
||||
t[base + 3] = null;
|
||||
t[base + 4] = null;
|
||||
t[base] = prefix;
|
||||
}
|
||||
final int k = base + 1 + (slot == 2 ? 1 : 0) + (type == null ? 0 : 2);
|
||||
String p = t[k];
|
||||
if (p == null) {
|
||||
p = format(prefix, slot, type);
|
||||
t[k] = p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private static String format(String prefix, int slot, String type) {
|
||||
return "textures/models/armor/" + prefix + (slot == 2 ? "_layer_2" : "_layer_1") + (type == null ? ".png" : "_" + type + ".png");
|
||||
}
|
||||
|
||||
private ArmorTexturePaths() {}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.joml.Matrix4fc;
|
||||
|
||||
public final class GlintClock {
|
||||
|
||||
private static final double SPEED0 = 0.001;
|
||||
private static final double SPEED1 = 0.001 + 0.003;
|
||||
private static final double SIN0 = Math.sin(Math.toRadians(30.0));
|
||||
private static final double COS0 = Math.cos(Math.toRadians(30.0));
|
||||
private static final double SIN1 = Math.sin(Math.toRadians(-30.0));
|
||||
private static final double COS1 = Math.cos(Math.toRadians(-30.0));
|
||||
|
||||
private static long millis;
|
||||
private static float armorU0, armorV0, armorU1, armorV1;
|
||||
|
||||
public static void beginFrame(long nowMillis) {
|
||||
millis = nowMillis;
|
||||
final double ticks = nowMillis / 50.0;
|
||||
|
||||
final double f0 = ticks * SPEED0 * 20.0;
|
||||
final double du0 = frac(-f0 * SIN0 / 3.0);
|
||||
final double dv0 = frac(f0 * COS0 / 3.0);
|
||||
armorU0 = (float) (3.0 * (COS0 * du0 + SIN0 * dv0));
|
||||
armorV0 = (float) (3.0 * (-SIN0 * du0 + COS0 * dv0));
|
||||
|
||||
final double f1 = ticks * SPEED1 * 20.0;
|
||||
final double du1 = frac(-f1 * SIN1 / 3.0);
|
||||
final double dv1 = frac(f1 * COS1 / 3.0);
|
||||
armorU1 = (float) (3.0 * (COS1 * du1 + SIN1 * dv1));
|
||||
armorV1 = (float) (3.0 * (-SIN1 * du1 + COS1 * dv1));
|
||||
}
|
||||
|
||||
public static long millis() {
|
||||
return millis;
|
||||
}
|
||||
|
||||
static boolean secondArmorLayer(Matrix4fc textureMatrix) {
|
||||
return textureMatrix.m01() < 0f;
|
||||
}
|
||||
|
||||
public static void translateArmorGlint() {
|
||||
if (secondArmorLayer(GLStateManager.getMatrixStack())) {
|
||||
GLStateManager.glTranslatef(armorU1, armorV1, 0f);
|
||||
} else {
|
||||
GLStateManager.glTranslatef(armorU0, armorV0, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private static double frac(double x) {
|
||||
return x - Math.floor(x);
|
||||
}
|
||||
|
||||
private GlintClock() {}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
public final class OperationArgs {
|
||||
|
||||
private OperationArgs() {}
|
||||
|
||||
public static Object boxed(Object prev, float v) {
|
||||
return prev instanceof Float f && Float.floatToRawIntBits(f) == Float.floatToRawIntBits(v) ? prev : Float.valueOf(v);
|
||||
}
|
||||
|
||||
public static Object boxed(Object prev, int v) {
|
||||
return prev instanceof Integer i && i == v ? prev : Integer.valueOf(v);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,10 @@ public final class ParticleRunSplitter {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean currentRunTranslucent() {
|
||||
return runTranslucent;
|
||||
}
|
||||
|
||||
public static boolean isTranslucent(EntityFX particle) {
|
||||
if (particle.particleIcon instanceof TextureAtlasSprite sprite) {
|
||||
return ((SpriteExtension) sprite).celeritas$getTransparencyLevel() == SpriteTransparencyLevel.TRANSLUCENT;
|
||||
|
||||
+15
-6
@@ -165,6 +165,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
final short originalBlockId = blockRenderContext.blockId;
|
||||
|
||||
int stateIdx = 0;
|
||||
int facesAtBaseMaterial = 0;
|
||||
boolean hasCachedMaterial = false;
|
||||
TextureAtlasSprite lastSprite = null;
|
||||
Material lastMaterial = null;
|
||||
@@ -255,14 +256,22 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
// Apply RGB block light tint from provider
|
||||
applyBlockLightTint(worldX, worldY, worldZ, vertices);
|
||||
|
||||
final int faceBit = 1 << facing.ordinal();
|
||||
final Material correctMaterial;
|
||||
if (hasCachedMaterial && sprite == lastSprite) {
|
||||
correctMaterial = lastMaterial;
|
||||
if ((facesAtBaseMaterial & faceBit) != 0) {
|
||||
correctMaterial = material;
|
||||
} else {
|
||||
correctMaterial = selectMaterial(material, sprite, isShaderPackOverride, useRenderPassOptimization);
|
||||
lastSprite = sprite;
|
||||
lastMaterial = correctMaterial;
|
||||
hasCachedMaterial = true;
|
||||
if (hasCachedMaterial && sprite == lastSprite) {
|
||||
correctMaterial = lastMaterial;
|
||||
} else {
|
||||
correctMaterial = selectMaterial(material, sprite, isShaderPackOverride, useRenderPassOptimization);
|
||||
lastSprite = sprite;
|
||||
lastMaterial = correctMaterial;
|
||||
hasCachedMaterial = true;
|
||||
}
|
||||
if (correctMaterial == material) {
|
||||
facesAtBaseMaterial |= faceBit;
|
||||
}
|
||||
}
|
||||
final var builder = buffers.get(correctMaterial);
|
||||
|
||||
|
||||
+85
-25
@@ -10,6 +10,7 @@ import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
|
||||
import com.gtnewhorizons.angelica.rendering.culling.GpuCulling;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
|
||||
import com.gtnewhorizons.angelica.rendering.voxelization.SdlShadowVoxelizationSink;
|
||||
import com.gtnewhorizons.angelica.rendering.voxelization.ShadowVoxelizer;
|
||||
@@ -48,7 +49,9 @@ import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
import org.embeddedt.embeddium.impl.render.viewport.CameraTransform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.opengl.EXTTextureFilterAnisotropic;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -61,9 +64,14 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
|
||||
|
||||
private GlProgram<? extends ChunkShaderInterface> irisProgram;
|
||||
private boolean usingIrisProgram;
|
||||
private int rgssSampler;
|
||||
private boolean rgssSamplerResolved;
|
||||
private boolean rgssSamplerBound;
|
||||
private int terrainSampler;
|
||||
private boolean terrainSamplerResolved;
|
||||
private int terrainSamplerAnisotropy = -1;
|
||||
private boolean terrainSamplerNearest;
|
||||
private int packTerrainSampler;
|
||||
private boolean packTerrainSamplerResolved;
|
||||
private int packTerrainSamplerAnisotropy = -1;
|
||||
private boolean terrainSamplerBound;
|
||||
private final GpuTerrainCuller culler;
|
||||
private final ReusableCachedBatch gpuBatch = new ReusableCachedBatch();
|
||||
|
||||
@@ -161,6 +169,7 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
|
||||
this.activeProgram = (GlProgram<ChunkShaderInterface>) override;
|
||||
this.irisProgram = override;
|
||||
this.usingIrisProgram = true;
|
||||
bindPackTerrainSampler();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -169,7 +178,7 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
|
||||
this.usingIrisProgram = false;
|
||||
this.irisProgram = null;
|
||||
super.begin(pass);
|
||||
bindRgssSampler();
|
||||
bindTerrainSampler();
|
||||
} finally {
|
||||
if (Tracy.ENABLED) Tracy.endZone();
|
||||
}
|
||||
@@ -177,7 +186,7 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
|
||||
|
||||
@Override
|
||||
protected void end(TerrainRenderPass pass) {
|
||||
unbindRgssSampler();
|
||||
unbindTerrainSampler();
|
||||
|
||||
if (usingIrisProgram && irisProgram != null) {
|
||||
irisProgram.getInterface().restoreState();
|
||||
@@ -192,32 +201,77 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
|
||||
super.end(pass);
|
||||
}
|
||||
|
||||
private void bindRgssSampler() {
|
||||
if (!AngelicaRenderPassConfiguration.isRgssEnabled()) {
|
||||
private void bindTerrainSampler() {
|
||||
if (!SodiumGameOptions.usesTerrainTexelSnap()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rgssSamplerResolved) {
|
||||
rgssSamplerResolved = true;
|
||||
rgssSampler = RenderSystem.genSampler();
|
||||
if (rgssSampler != 0) {
|
||||
RenderSystem.samplerParameteri(rgssSampler, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
|
||||
RenderSystem.samplerParameteri(rgssSampler, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
|
||||
} else {
|
||||
AngelicaMod.LOGGER.warn("Sampler objects unavailable; RGSS terrain filtering will fall back to nearest sampling");
|
||||
final int anisotropy = SodiumGameOptions.resolvedAnisotropicFiltering();
|
||||
final boolean nearest = ClientProxy.options().quality.texelSampling.isNearest();
|
||||
|
||||
if (!terrainSamplerResolved || terrainSamplerAnisotropy != anisotropy
|
||||
|| terrainSamplerNearest != nearest) {
|
||||
if (!terrainSamplerResolved) {
|
||||
terrainSampler = RenderSystem.genSampler();
|
||||
if (terrainSampler == 0) {
|
||||
AngelicaMod.LOGGER.warn("Sampler objects unavailable; terrain filtering will fall back to the atlas texture's own parameters");
|
||||
}
|
||||
}
|
||||
terrainSamplerResolved = true;
|
||||
terrainSamplerAnisotropy = anisotropy;
|
||||
terrainSamplerNearest = nearest;
|
||||
|
||||
configureTerrainSampler(terrainSampler, nearest, anisotropy);
|
||||
}
|
||||
|
||||
if (rgssSampler != 0) {
|
||||
RenderSystem.bindSamplerToUnit(BLOCK_TEXTURE_UNIT, rgssSampler);
|
||||
rgssSamplerBound = true;
|
||||
if (terrainSampler != 0) {
|
||||
RenderSystem.bindSamplerToUnit(BLOCK_TEXTURE_UNIT, terrainSampler);
|
||||
terrainSamplerBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void unbindRgssSampler() {
|
||||
if (rgssSamplerBound) {
|
||||
private void bindPackTerrainSampler() {
|
||||
final int anisotropy = SodiumGameOptions.resolvedAnisotropicFiltering();
|
||||
|
||||
if (!packTerrainSamplerResolved || packTerrainSamplerAnisotropy != anisotropy) {
|
||||
if (!packTerrainSamplerResolved) {
|
||||
packTerrainSampler = RenderSystem.genSampler();
|
||||
if (packTerrainSampler == 0) {
|
||||
AngelicaMod.LOGGER.warn("Sampler objects unavailable; shader pack terrain filtering will fall back to the atlas texture's own parameters");
|
||||
}
|
||||
}
|
||||
packTerrainSamplerResolved = true;
|
||||
packTerrainSamplerAnisotropy = anisotropy;
|
||||
|
||||
configureTerrainSampler(packTerrainSampler, true, anisotropy);
|
||||
}
|
||||
|
||||
if (packTerrainSampler != 0) {
|
||||
RenderSystem.bindSamplerToUnit(BLOCK_TEXTURE_UNIT, packTerrainSampler);
|
||||
terrainSamplerBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureTerrainSampler(int sampler, boolean nearest, int anisotropy) {
|
||||
if (sampler == 0) {
|
||||
return;
|
||||
}
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_MIN_FILTER,
|
||||
nearest ? GL11.GL_NEAREST_MIPMAP_LINEAR : GL11.GL_LINEAR_MIPMAP_LINEAR);
|
||||
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_MAG_FILTER,
|
||||
nearest ? GL11.GL_NEAREST : GL11.GL_LINEAR);
|
||||
if (SodiumGameOptions.anisotropySupported()) {
|
||||
RenderSystem.samplerParameterf(sampler,
|
||||
EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT, (float) anisotropy);
|
||||
}
|
||||
}
|
||||
|
||||
private void unbindTerrainSampler() {
|
||||
if (terrainSamplerBound) {
|
||||
RenderSystem.bindSamplerToUnit(BLOCK_TEXTURE_UNIT, 0);
|
||||
rgssSamplerBound = false;
|
||||
terrainSamplerBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,10 +283,16 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
|
||||
culler.delete();
|
||||
}
|
||||
|
||||
unbindRgssSampler();
|
||||
RenderSystem.destroySampler(rgssSampler);
|
||||
rgssSampler = 0;
|
||||
rgssSamplerResolved = false;
|
||||
unbindTerrainSampler();
|
||||
RenderSystem.destroySampler(terrainSampler);
|
||||
terrainSampler = 0;
|
||||
terrainSamplerResolved = false;
|
||||
terrainSamplerAnisotropy = -1;
|
||||
terrainSamplerNearest = false;
|
||||
RenderSystem.destroySampler(packTerrainSampler);
|
||||
packTerrainSampler = 0;
|
||||
packTerrainSamplerResolved = false;
|
||||
packTerrainSamplerAnisotropy = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-2
@@ -2,10 +2,10 @@ package com.gtnewhorizons.angelica.rendering.celeritas;
|
||||
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.utils.SpritePadding;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
|
||||
import lombok.Getter;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.options.named.TextureFilterMode;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.compile.sorting.QuadPrimitiveType;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
|
||||
@@ -30,15 +30,27 @@ public class AngelicaRenderPassConfiguration {
|
||||
.vertexType(vertexType)
|
||||
.primitiveType(QuadPrimitiveType.TRIANGULATED);
|
||||
|
||||
final int mipmapLevels = SodiumGameOptions.terrainMipmapLevels();
|
||||
|
||||
if (rgssEnabled) {
|
||||
builder.extraDefine("USE_RGSS", "");
|
||||
}
|
||||
if (SodiumGameOptions.usesTerrainTexelSnap()) {
|
||||
builder.extraDefine("USE_TEXEL_SNAP", "");
|
||||
}
|
||||
if (SodiumGameOptions.effectiveTextureFilterMode().usesAnisotropy()) {
|
||||
builder.extraDefine("USE_ANISOTROPIC", "");
|
||||
builder.extraDefine("TERRAIN_GUTTER", SpritePadding.gutterFor(mipmapLevels, true) + ".0");
|
||||
}
|
||||
if (mipmapLevels <= 0) {
|
||||
builder.extraDefine("TERRAIN_NO_MIPS", "");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static RenderPassConfiguration<BlockRenderLayer> build(ChunkVertexType vertexType) {
|
||||
rgssEnabled = SodiumGameOptions.effectiveTextureFilterMode() == TextureFilterMode.RGSS;
|
||||
rgssEnabled = SodiumGameOptions.effectiveTextureFilterMode().usesRgss();
|
||||
|
||||
SOLID_PASS = builderForRenderType(0, true, vertexType)
|
||||
.name("solid")
|
||||
|
||||
+1
@@ -88,6 +88,7 @@ public class CeleritasDebugScreenHandler {
|
||||
addIfPresent(lines, RenderClassTimings.SHADOW_ENTITY.debugLine());
|
||||
addIfPresent(lines, BailClassCounts.MATERIAL.debugLine());
|
||||
addIfPresent(lines, BailClassCounts.TEMPLATE.debugLine());
|
||||
addIfPresent(lines, BailClassCounts.PARTICLE_SPILL.debugLine());
|
||||
}
|
||||
}
|
||||
lines.add("");
|
||||
|
||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user