diff --git a/src/generated/resources/assets/anvilcraft/lang/en_ud.json b/src/generated/resources/assets/anvilcraft/lang/en_ud.json index 72ccaf05d1..cb6cd84abf 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_ud.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_ud.json @@ -145,6 +145,21 @@ "anvilcraft.configuration.giant_anvil_max_shock_radius.tooltip": "ɹoᴉʌɐɥǝq ʞɔoɥs s,ꞁᴉʌuɐ ʇuɐᴉᵷ ɟo snᴉpɐɹ ɯnɯᴉxɐW", "anvilcraft.configuration.goggle_mode": "ǝpoW ǝꞁᵷᵷo⅁", "anvilcraft.configuration.goggle_mode.tooltip": "oɟuᴉ ǝꞁᵷᵷoᵷ ɹǝɯɯɐɥ ꞁᴉʌuɐ ǝɥʇ ɟo ǝpoɯ ǝɥ⟘", + "anvilcraft.configuration.gravitational_lens": "suǝꞀ ꞁɐuoᴉʇɐʇᴉʌɐɹ⅁", + "anvilcraft.configuration.gravitational_lens.button": "suǝꞀ ꞁɐuoᴉʇɐʇᴉʌɐɹ⅁", + "anvilcraft.configuration.gravitational_lens.event_horizon_radius": "snᴉpɐᴚ uozᴉɹoH ʇuǝʌƎ", + "anvilcraft.configuration.gravitational_lens.event_horizon_radius.tooltip": "(ʇꞁnɐɟǝp Ɛ80˙0 'sʇᴉun Ʌ∩ uǝǝɹɔs) snᴉpɐɹ uozᴉɹoɥ ʇuǝʌƎ", + "anvilcraft.configuration.gravitational_lens.lens_direction": "uoᴉʇɔǝɹᴉᗡ suǝꞀ", + "anvilcraft.configuration.gravitational_lens.lens_direction.tooltip": "˙ɥʇᵷuǝɹʇs sǝꞁɐɔs ǝpnʇᴉuᵷɐW ˙(ʎɐʍɐ ɥsnd) ǝʌɐɔuoɔ = 0> '(ɹǝʇuǝɔ pɹɐʍoʇ ꞁꞁnd ꞁɐuoᴉʇɐʇᴉʌɐɹᵷ) xǝʌuoɔ = 0< :uoᴉʇɔǝɹᴉp suǝꞀ", + "anvilcraft.configuration.gravitational_lens.lens_perspective_scale": "ǝꞁɐɔS ǝʌᴉʇɔǝdsɹǝԀ suǝꞀ", + "anvilcraft.configuration.gravitational_lens.lens_perspective_scale.tooltip": "˙ɹǝᵷᵷᴉq = ɹǝsoꞁƆ ˙ǝzᴉs ᵷᴉɟuoɔ = ʇɔǝɟɟǝ 'ǝɔuɐʇsᴉp sᴉɥʇ ʇⱯ ˙ᵷuᴉꞁɐɔs ǝʌᴉʇɔǝdsɹǝd ɹoɟ ǝɔuɐʇsᴉp ǝɔuǝɹǝɟǝᴚ", + "anvilcraft.configuration.gravitational_lens.lens_strength": "ɥʇᵷuǝɹʇS suǝꞀ", + "anvilcraft.configuration.gravitational_lens.lens_strength.tooltip": "(ʇꞁnɐɟǝp ᘔ00˙0 'ᵷuᴉpuǝq ɹǝᵷuoɹʇs = ɹǝɥᵷᴉɥ) ɥʇᵷuǝɹʇs uoᴉʇɹoʇsᴉp suǝꞀ", + "anvilcraft.configuration.gravitational_lens.max_hole_count": "ʇunoƆ ǝꞁoH xɐW", + "anvilcraft.configuration.gravitational_lens.max_hole_count.tooltip": "˙ǝɔuɐɯɹoɟɹǝd ɹǝʇʇǝq = ɹǝʍoꞁ 'sǝꞁoɥ ǝɹoɯ = ɹǝɥᵷᴉH ˙(9ϛᘔ-ᘔ) pǝɹǝpuǝɹ sǝꞁoɥ ǝʇᴉɥʍ/ʞɔɐꞁq ɟo ɹǝqɯnu ɯnɯᴉxɐW", + "anvilcraft.configuration.gravitational_lens.render_black_hole_lensing": "ᵷuᴉsuǝꞀ ǝꞁoH ʞɔɐꞁᗺ ɹǝpuǝᴚ", + "anvilcraft.configuration.gravitational_lens.render_black_hole_lensing.tooltip": "sǝꞁoɥ ʞɔɐꞁq ɹɐǝu ʇɔǝɟɟǝ ᵷuᴉssǝɔoɹd-ʇsod ᵷuᴉsuǝꞁ ꞁɐuoᴉʇɐʇᴉʌɐɹ⅁", + "anvilcraft.configuration.gravitational_lens.tooltip": "suǝꞀ ꞁɐuoᴉʇɐʇᴉʌɐɹ⅁", "anvilcraft.configuration.ground_heave_particle_chance": "ǝɔuɐɥƆ ǝꞁɔᴉʇɹɐԀ ǝʌɐǝH punoɹ⅁", "anvilcraft.configuration.ground_heave_particle_chance.tooltip": "sǝꞁɔᴉʇɹɐd ǝʌɐǝɥ punoɹᵷ suʍɐds ʞɔoꞁq ɥɔɐǝ (0˙⥝-0˙0) ʎʇᴉꞁᴉqɐqoɹԀ", "anvilcraft.configuration.ground_heave_particle_count": "ʇunoƆ ǝꞁɔᴉʇɹɐԀ ǝʌɐǝH punoɹ⅁", diff --git a/src/generated/resources/assets/anvilcraft/lang/en_us.json b/src/generated/resources/assets/anvilcraft/lang/en_us.json index 662897170b..3dc588393a 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_us.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_us.json @@ -145,6 +145,21 @@ "anvilcraft.configuration.giant_anvil_max_shock_radius.tooltip": "Maximum radius of giant anvil's shock behavior", "anvilcraft.configuration.goggle_mode": "Goggle Mode", "anvilcraft.configuration.goggle_mode.tooltip": "The mode of the anvil hammer goggle info", + "anvilcraft.configuration.gravitational_lens": "Gravitational Lens", + "anvilcraft.configuration.gravitational_lens.button": "Gravitational Lens", + "anvilcraft.configuration.gravitational_lens.event_horizon_radius": "Event Horizon Radius", + "anvilcraft.configuration.gravitational_lens.event_horizon_radius.tooltip": "Event horizon radius (screen UV units, 0.083 default)", + "anvilcraft.configuration.gravitational_lens.lens_direction": "Lens Direction", + "anvilcraft.configuration.gravitational_lens.lens_direction.tooltip": "Lens direction: >0 = convex (gravitational pull toward center), <0 = concave (push away). Magnitude scales strength.", + "anvilcraft.configuration.gravitational_lens.lens_perspective_scale": "Lens Perspective Scale", + "anvilcraft.configuration.gravitational_lens.lens_perspective_scale.tooltip": "Reference distance for perspective scaling. At this distance, effect = config size. Closer = bigger.", + "anvilcraft.configuration.gravitational_lens.lens_strength": "Lens Strength", + "anvilcraft.configuration.gravitational_lens.lens_strength.tooltip": "Lens distortion strength (higher = stronger bending, 0.002 default)", + "anvilcraft.configuration.gravitational_lens.max_hole_count": "Max Hole Count", + "anvilcraft.configuration.gravitational_lens.max_hole_count.tooltip": "Maximum number of black/white holes rendered (2-256). Higher = more holes, lower = better performance.", + "anvilcraft.configuration.gravitational_lens.render_black_hole_lensing": "Render Black Hole Lensing", + "anvilcraft.configuration.gravitational_lens.render_black_hole_lensing.tooltip": "Gravitational lensing post-processing effect near black holes", + "anvilcraft.configuration.gravitational_lens.tooltip": "Gravitational Lens", "anvilcraft.configuration.ground_heave_particle_chance": "Ground Heave Particle Chance", "anvilcraft.configuration.ground_heave_particle_chance.tooltip": "Probability (0.0-1.0) each block spawns ground heave particles", "anvilcraft.configuration.ground_heave_particle_count": "Ground Heave Particle Count", diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/BlackHoleBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/BlackHoleBlockEntity.java index 25c4a8e7a4..67ed295564 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/BlackHoleBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/BlackHoleBlockEntity.java @@ -1,5 +1,6 @@ package dev.dubhe.anvilcraft.block.entity; +import dev.dubhe.anvilcraft.client.support.GravitationalLensManager; import dev.dubhe.anvilcraft.init.block.ModBlockEntities; import dev.dubhe.anvilcraft.util.GravityManager; import net.minecraft.core.BlockPos; @@ -19,10 +20,14 @@ public static BlackHoleBlockEntity createBlockEntity(BlockEntityType type, Bl @Override public void onLoad() { super.onLoad(); - if (this.level != null && !this.level.isClientSide) { - GravityManager.GravitySourceType type = GravityManager.GravitySourceManager.getType(this.getBlockState().getBlock()); - if (type != null) { - GravityManager.GravitySourceManager.addSource(this.level, this.worldPosition, type); + if (this.level != null) { + if (this.level.isClientSide) { + GravitationalLensManager.register(this.worldPosition); + } else { + GravityManager.GravitySourceType type = GravityManager.GravitySourceManager.getType(this.getBlockState().getBlock()); + if (type != null) { + GravityManager.GravitySourceManager.addSource(this.level, this.worldPosition, type); + } } } } @@ -30,8 +35,12 @@ public void onLoad() { @Override public void setRemoved() { super.setRemoved(); - if (this.level != null && !this.level.isClientSide) { - GravityManager.GravitySourceManager.removeSource(this.level, this.worldPosition); + if (this.level != null) { + if (this.level.isClientSide) { + GravitationalLensManager.unregister(this.worldPosition); + } else { + GravityManager.GravitySourceManager.removeSource(this.level, this.worldPosition); + } } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/WhiteHoleBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/WhiteHoleBlockEntity.java index 49701b5748..3e8a932d63 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/WhiteHoleBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/WhiteHoleBlockEntity.java @@ -1,5 +1,6 @@ package dev.dubhe.anvilcraft.block.entity; +import dev.dubhe.anvilcraft.client.support.GravitationalLensManager; import dev.dubhe.anvilcraft.init.block.ModBlockEntities; import dev.dubhe.anvilcraft.util.GravityManager; import net.minecraft.core.BlockPos; @@ -19,10 +20,14 @@ public static WhiteHoleBlockEntity createBlockEntity(BlockEntityType type, Bl @Override public void onLoad() { super.onLoad(); - if (this.level != null && !this.level.isClientSide) { - GravityManager.GravitySourceType type = GravityManager.GravitySourceManager.getType(this.getBlockState().getBlock()); - if (type != null) { - GravityManager.GravitySourceManager.addSource(this.level, this.worldPosition, type); + if (this.level != null) { + if (this.level.isClientSide) { + GravitationalLensManager.registerWhiteHole(this.worldPosition); + } else { + GravityManager.GravitySourceType type = GravityManager.GravitySourceManager.getType(this.getBlockState().getBlock()); + if (type != null) { + GravityManager.GravitySourceManager.addSource(this.level, this.worldPosition, type); + } } } } @@ -30,8 +35,12 @@ public void onLoad() { @Override public void setRemoved() { super.setRemoved(); - if (this.level != null && !this.level.isClientSide) { - GravityManager.GravitySourceManager.removeSource(this.level, this.worldPosition); + if (this.level != null) { + if (this.level.isClientSide) { + GravitationalLensManager.unregisterWhiteHole(this.worldPosition); + } else { + GravityManager.GravitySourceManager.removeSource(this.level, this.worldPosition); + } } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java b/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java index e4b34ea7f2..2579faea6f 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/init/ModShaders.java @@ -2,6 +2,7 @@ import com.mojang.blaze3d.vertex.DefaultVertexFormat; import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.client.support.GravitationalLensManager; import lombok.Getter; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.PostChain; @@ -15,9 +16,12 @@ public class ModShaders { public static final ResourceLocation LASER_BLOOM_LOCATION = AnvilCraft.of("shaders/post/bloom.json"); + public static final ResourceLocation GRAVITATIONAL_LENS_LOCATION = AnvilCraft.of("shaders/post/gravitational_lens.json"); @Getter private static PostChain bloomChain; + @Getter + private static PostChain lensChain; static final Minecraft MINECRAFT = Minecraft.getInstance(); @Getter @@ -38,7 +42,8 @@ public class ModShaders { public static void register(RegisterShadersEvent event) { try { - event.registerShader(new ShaderInstance( + event.registerShader( + new ShaderInstance( event.getResourceProvider(), AnvilCraft.of("rendertype_laser"), DefaultVertexFormat.BLOCK @@ -102,6 +107,9 @@ public static void resize(int width, int height) { if (bloomChain != null) { bloomChain.resize(width, height); } + if (lensChain != null) { + lensChain.resize(width, height); + } orthoMatrix = new Matrix4f() .setOrtho( 0f, @@ -132,4 +140,22 @@ public static void loadBloomEffect(ResourceProvider resourceProvider) throws IOE AnvilCraft.LOGGER.error("Could not load bloom effect shader.", tr); } } + + public static void loadLensEffect(ResourceProvider resourceProvider) throws IOException { + GravitationalLensManager.resetLensUbo(); + try { + lensChain = new PostChain( + MINECRAFT.getTextureManager(), + resourceProvider, + Minecraft.getInstance().getMainRenderTarget(), + GRAVITATIONAL_LENS_LOCATION + ); + lensChain.resize( + Minecraft.getInstance().getWindow().getWidth(), + Minecraft.getInstance().getWindow().getHeight() + ); + } catch (Throwable tr) { + AnvilCraft.LOGGER.error("Could not load gravitational lens effect shader.", tr); + } + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/RenderState.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/RenderState.java index 92aa4094df..0301ab098e 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/RenderState.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/RenderState.java @@ -38,4 +38,8 @@ public static boolean isBloomEffectEnabled() { public static boolean isScanPreviewEffectEnabled() { return AnvilCraftClient.CONFIG.renderScanPreviewEffect; } + + public static boolean isLensEffectEnabled() { + return isEnhancedRenderingAvailable() && AnvilCraftClient.CONFIG.gravitationalLens.renderBlackHoleLensing; + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/support/GravitationalLensManager.java b/src/main/java/dev/dubhe/anvilcraft/client/support/GravitationalLensManager.java new file mode 100644 index 0000000000..c632756220 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/client/support/GravitationalLensManager.java @@ -0,0 +1,239 @@ +package dev.dubhe.anvilcraft.client.support; + +import net.minecraft.client.Camera; +import net.minecraft.core.BlockPos; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.joml.Vector4f; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL31; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; + +public class GravitationalLensManager { + private static final int MAX_SEARCH_DISTANCE_SQR = 256 * 256; + + /** + * Client-side cache of loaded black hole block positions. + */ + public static final Set CLIENT_BLACK_HOLE_POSITIONS = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + /** + * Client-side cache of loaded white hole block positions. + */ + public static final Set CLIENT_WHITE_HOLE_POSITIONS = + Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public static void register(BlockPos pos) { + CLIENT_BLACK_HOLE_POSITIONS.add(pos.immutable()); + } + + public static void unregister(BlockPos pos) { + CLIENT_BLACK_HOLE_POSITIONS.remove(pos); + } + + public static void registerWhiteHole(BlockPos pos) { + CLIENT_WHITE_HOLE_POSITIONS.add(pos.immutable()); + } + + public static void unregisterWhiteHole(BlockPos pos) { + CLIENT_WHITE_HOLE_POSITIONS.remove(pos); + } + + /** + * Per-hole data passed to the shader. + */ + public static final class HoleProjection { + /** + * Center UV of the black hole on screen. + */ + public final float centerU; + public final float centerV; + /** + * Distance from camera to black hole (world units). + */ + public final float cameraDistance; + /** + * Lens direction: > 0 = convex (pull), < 0 = concave (push). + */ + public final float lensDirection; + + HoleProjection(float cu, float cv, float dist, float dir) { + this.centerU = cu; + this.centerV = cv; + this.cameraDistance = dist; + this.lensDirection = dir; + } + } + + /** + * Build the combined view-projection matrix from camera position/rotation + projection. + */ + private static Matrix4f buildViewProj(Camera camera, Matrix4f projectionMatrix) { + float yaw = camera.getYRot(); + float pitch = camera.getXRot(); + + Quaternionf cameraRotation = new Quaternionf() + .rotateX((float) Math.toRadians(pitch)) + .rotateY((float) Math.toRadians(yaw + 180.0f)); + + Vector3f cameraPos = camera.getPosition().toVector3f(); + + Matrix4f viewMatrix = new Matrix4f() + .rotate(cameraRotation) + .translate(-cameraPos.x, -cameraPos.y, -cameraPos.z); + + return new Matrix4f(projectionMatrix).mul(viewMatrix); + } + + /** + * Transform a world-space point to screen UV via view-projection. + * Returns {@code null} when the point is behind the camera (clip.w ≤ 0). + */ + private static @Nullable Vector2f worldToScreenUV(float wx, float wy, float wz, Matrix4f viewProj) { + Vector4f clip = viewProj.transform(new Vector4f(wx, wy, wz, 1.0f)); + if (clip.w <= 0.0f) return null; + + float ndcX = clip.x / clip.w; + float ndcY = clip.y / clip.w; + + // Clamp to screen edge — still useful for points slightly off-screen + ndcX = Math.clamp(ndcX, -1.0f, 1.0f); + ndcY = Math.clamp(ndcY, -1.0f, 1.0f); + + return new Vector2f((ndcX + 1.0f) / 2.0f, (ndcY + 1.0f) / 2.0f); + } + + /** + * Collect up to {@code maxCount} on-screen holes from both black and white hole sets, + * sorted nearest first. Black holes get {@code blackHoleDir} (positive=convex pull), + * white holes get {@code whiteHoleDir} (negative=concave push). + */ + public static List collectVisibleHoles( + Camera camera, + Matrix4f projectionMatrix, + int maxCount, + float blackHoleDir, + float whiteHoleDir + ) { + List result = new ArrayList<>(); + + Matrix4f viewProj = buildViewProj(camera, projectionMatrix); + Vector3f cameraPos = camera.getPosition().toVector3f(); + + collectFromSet(CLIENT_BLACK_HOLE_POSITIONS, cameraPos, viewProj, blackHoleDir, result); + collectFromSet(CLIENT_WHITE_HOLE_POSITIONS, cameraPos, viewProj, whiteHoleDir, result); + + // Sort nearest first, then take the closest maxCount + result.sort((a, b) -> Float.compare(a.cameraDistance, b.cameraDistance)); + if (result.size() > maxCount) { + result = result.subList(0, maxCount); + } + return result; + } + + private static void collectFromSet( + Set positions, Vector3f cameraPos, Matrix4f viewProj, + float lensDir, List out + ) { + for (BlockPos pos : positions) { + double dx = pos.getX() + 0.5 - cameraPos.x; + double dy = pos.getY() + 0.5 - cameraPos.y; + double dz = pos.getZ() + 0.5 - cameraPos.z; + double distanceSqr = dx * dx + dy * dy + dz * dz; + if (distanceSqr > MAX_SEARCH_DISTANCE_SQR) continue; + + Vector2f centerUV = worldToScreenUV( + pos.getX() + 0.5f, pos.getY() + 0.5f, pos.getZ() + 0.5f, viewProj + ); + if (centerUV == null) continue; + + if (centerUV.x < -0.2f || centerUV.x > 1.2f + || centerUV.y < -0.2f || centerUV.y > 1.2f) { + continue; + } + + float dist = (float) Math.sqrt(distanceSqr); + out.add(new HoleProjection(centerUV.x, centerUV.y, dist, lensDir)); + } + } + + // ---- UBO management ---- + + /** + * Pre-allocated FloatBuffer for UBO upload (256 vec4s × 4 floats = 4096 bytes). + */ + private static final FloatBuffer LENS_UBO_BUF = + ByteBuffer.allocateDirect(256 * 4 * 4) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + /** + * UBO handle — created on first frame, reset on shader reload. + */ + private static int lensUbo = 0; + /** + * Last program ID for which the UBO block index was bound. + */ + private static int lensUboBlockBound = 0; + + /** + * Upload hole data to the UBO and bind it. Call each frame before running the lens post-chain. + * + * @param holes collected hole projections (≤ 256) + * @param count actual number of holes to write (remaining slots zeroed) + * @param programId the shader program ID, for one-time block-index binding + */ + public static void uploadLensUbo(List holes, int count, int programId) { + FloatBuffer buf = LENS_UBO_BUF; + buf.clear(); + for (int i = 0; i < 256; i++) { + if (i < count) { + HoleProjection h = holes.get(i); + buf.put(h.centerU).put(h.centerV).put(h.cameraDistance).put(h.lensDirection); + } else { + buf.put(0.0f).put(0.0f).put(1.0f).put(1.0f); + } + } + buf.flip(); + + if (lensUbo == 0) { + lensUbo = GL15.glGenBuffers(); + GL15.glBindBuffer(GL31.GL_UNIFORM_BUFFER, lensUbo); + GL15.glBufferData(GL31.GL_UNIFORM_BUFFER, buf, GL15.GL_DYNAMIC_DRAW); + } else { + GL15.glBindBuffer(GL31.GL_UNIFORM_BUFFER, lensUbo); + GL15.glBufferSubData(GL31.GL_UNIFORM_BUFFER, 0, buf); + } + GL30.glBindBufferBase(GL31.GL_UNIFORM_BUFFER, 0, lensUbo); + + // Bind UBO block "BlackHoles" to binding point 0 (once per shader load) + if (lensUboBlockBound != programId) { + int blockIndex = GL31.glGetUniformBlockIndex(programId, "BlackHoles"); + if (blockIndex != GL31.GL_INVALID_INDEX) { + GL31.glUniformBlockBinding(programId, blockIndex, 0); + } + lensUboBlockBound = programId; + } + } + + /** + * Free the UBO and reset state. Call on shader reload / GL context recreation. + */ + public static void resetLensUbo() { + if (lensUbo != 0) { + GL15.glDeleteBuffers(lensUbo); + lensUbo = 0; + } + lensUboBlockBound = 0; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/config/AnvilCraftClientConfig.java b/src/main/java/dev/dubhe/anvilcraft/config/AnvilCraftClientConfig.java index e25577e67e..3c73675500 100644 --- a/src/main/java/dev/dubhe/anvilcraft/config/AnvilCraftClientConfig.java +++ b/src/main/java/dev/dubhe/anvilcraft/config/AnvilCraftClientConfig.java @@ -45,6 +45,30 @@ public class AnvilCraftClientConfig { @Comment("Scanline post-processing effect on 3D structure previews.") public boolean renderScanPreviewEffect = true; + @CollapsibleObject + public GravitationalLens gravitationalLens = new GravitationalLens(); + + public static class GravitationalLens { + @Comment("Gravitational lensing post-processing effect near black holes") + public boolean renderBlackHoleLensing = true; + + @Comment("Maximum number of black/white holes rendered (2-256). Higher = more holes, lower = better performance.") + @BoundedDiscrete(min = 2, max = 256) + public int maxHoleCount = 8; + + @Comment("Lens distortion strength (higher = stronger bending, 0.002 default)") + public double lensStrength = 1.0 / 512.0; + + @Comment("Event horizon radius (screen UV units, 0.083 default)") + public double eventHorizonRadius = 1.0 / 12.0; + + @Comment("Reference distance for perspective scaling. At this distance, effect = config size. Closer = bigger.") + public double lensPerspectiveScale = 10.0; + + @Comment("Lens direction: >0 = convex (gravitational pull toward center), <0 = concave (push away). Magnitude scales strength.") + public double lensDirection = 1.0; + } + @Comment("A vertical item frame vertically displays items") public boolean verticalItemFrame = false; diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/GameRendererMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/GameRendererMixin.java index c0e5049551..7992722e1a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/mixin/GameRendererMixin.java +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/GameRendererMixin.java @@ -24,6 +24,7 @@ abstract class GameRendererMixin { ) void loadBloomEffect(ResourceProvider resourceProvider, CallbackInfo ci) throws IOException { ModShaders.loadBloomEffect(resourceProvider); + ModShaders.loadLensEffect(resourceProvider); } @Inject( diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/LevelRendererMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/LevelRendererMixin.java index 47110f377c..d83649739a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/mixin/LevelRendererMixin.java +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/LevelRendererMixin.java @@ -12,9 +12,11 @@ import com.mojang.blaze3d.vertex.Tesselator; import com.mojang.blaze3d.vertex.VertexFormat; import dev.dubhe.anvilcraft.api.rendering.CacheableBERenderingPipeline; +import dev.dubhe.anvilcraft.client.AnvilCraftClient; import dev.dubhe.anvilcraft.client.init.ModRenderTargets; import dev.dubhe.anvilcraft.client.init.ModShaders; import dev.dubhe.anvilcraft.client.renderer.RenderState; +import dev.dubhe.anvilcraft.client.support.GravitationalLensManager; import dev.dubhe.anvilcraft.client.support.PowerGridSupport; import dev.dubhe.anvilcraft.client.support.RenderSupport; import net.minecraft.client.Camera; @@ -25,6 +27,8 @@ import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.PostChain; +import net.minecraft.client.renderer.PostPass; import net.minecraft.client.renderer.ShaderInstance; import org.joml.Matrix4f; import org.lwjgl.opengl.GL11; @@ -164,4 +168,80 @@ void bloomPostProcess( RenderSystem.enableDepthTest(); minecraft.getMainRenderTarget().bindWrite(false); } + + @Inject( + method = "renderLevel", + at = @At("TAIL") + ) + void gravitationalLensPostProcess( + DeltaTracker deltaTracker, + boolean renderBlockOutline, + Camera camera, + GameRenderer gameRenderer, + LightTexture lightTexture, + Matrix4f frustumMatrix, + Matrix4f projectionMatrix, + CallbackInfo ci + ) { + if (!RenderState.isLensEffectEnabled()) return; + if (ModShaders.getLensChain() == null) return; + + PostChain lensChain = ModShaders.getLensChain(); + java.util.List passes = ((PostChainAccessor) lensChain).getPasses(); + if (passes.isEmpty()) return; + + // Collect visible holes: black holes use positive direction, white holes negative + float dir = (float) AnvilCraftClient.CONFIG.gravitationalLens.lensDirection; + int maxCount = AnvilCraftClient.CONFIG.gravitationalLens.maxHoleCount; + java.util.List holes = + GravitationalLensManager.collectVisibleHoles(camera, projectionMatrix, maxCount, dir, -dir); + + int count = Math.min(holes.size(), maxCount); + + PostPass pass = passes.getFirst(); + + GravitationalLensManager.uploadLensUbo(holes, count, pass.getEffect().getId()); + + pass.getEffect().safeGetUniform("BlackHoleCount").set((float) count); + pass.getEffect().safeGetUniform("LensStrength") + .set((float) AnvilCraftClient.CONFIG.gravitationalLens.lensStrength); + pass.getEffect().safeGetUniform("EventHorizonRadius") + .set((float) AnvilCraftClient.CONFIG.gravitationalLens.eventHorizonRadius); + pass.getEffect().safeGetUniform("PerspectiveScale") + .set((float) AnvilCraftClient.CONFIG.gravitationalLens.lensPerspectiveScale); + + // Run the lens post chain (reads main target, writes to result target) + lensChain.process(RenderSupport.getPartialTick()); + + // Blit result back to the main render target + RenderTarget result = lensChain.getTempTarget("result"); + RenderTarget main = Minecraft.getInstance().getMainRenderTarget(); + + float width = main.width; + float height = main.height; + + ShaderInstance blitShader = ModShaders.getBlitShader(); + RenderSystem.viewport(0, 0, (int) width, (int) height); + blitShader.setSampler("DiffuseSampler", result); + blitShader.safeGetUniform("ProjMat").set(ModShaders.getOrthoMatrix()); + blitShader.safeGetUniform("OutSize").set(width, height); + RenderSystem.depthFunc(GL11.GL_ALWAYS); + + BufferBuilder bufferbuilder = Tesselator.getInstance() + .begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION); + bufferbuilder.addVertex(0.0F, 0.0F, 500.0F); + bufferbuilder.addVertex(width, 0.0F, 500.0F); + bufferbuilder.addVertex(width, height, 500.0F); + bufferbuilder.addVertex(0.0F, height, 500.0F); + + blitShader.apply(); + main.bindWrite(false); + BufferUploader.draw(bufferbuilder.buildOrThrow()); + main.unbindWrite(); + result.unbindRead(); + + RenderSystem.depthFunc(GL11.GL_LEQUAL); + RenderSystem.enableDepthTest(); + main.bindWrite(false); + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/PostChainAccessor.java b/src/main/java/dev/dubhe/anvilcraft/mixin/PostChainAccessor.java new file mode 100644 index 0000000000..bf677e7139 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/PostChainAccessor.java @@ -0,0 +1,14 @@ +package dev.dubhe.anvilcraft.mixin; + +import net.minecraft.client.renderer.PostChain; +import net.minecraft.client.renderer.PostPass; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; + +@Mixin(PostChain.class) +public interface PostChainAccessor { + @Accessor + List getPasses(); +} diff --git a/src/main/resources/anvilcraft.mixins.json b/src/main/resources/anvilcraft.mixins.json index ccd487e464..1e959e5566 100644 --- a/src/main/resources/anvilcraft.mixins.json +++ b/src/main/resources/anvilcraft.mixins.json @@ -91,6 +91,7 @@ "MinecraftClientMixin", "MouseHandlerMixin", "ParticleEngineMixin", + "PostChainAccessor", "SectionCompilerMixin", "SoundEngineMixin", "accessor.MultiPlayerGameModeAccessor", diff --git a/src/main/resources/assets/anvilcraft/shaders/post/gravitational_lens.json b/src/main/resources/assets/anvilcraft/shaders/post/gravitational_lens.json new file mode 100644 index 0000000000..8e5249eaac --- /dev/null +++ b/src/main/resources/assets/anvilcraft/shaders/post/gravitational_lens.json @@ -0,0 +1,13 @@ +{ + "targets": [ + "mcinput", + "result" + ], + "passes": [ + { + "name": "anvilcraft:gravitational_lens", + "intarget": "minecraft:main", + "outtarget": "result" + } + ] +} diff --git a/src/main/resources/assets/anvilcraft/shaders/program/gravitational_lens.fsh b/src/main/resources/assets/anvilcraft/shaders/program/gravitational_lens.fsh new file mode 100644 index 0000000000..ef2f4ad3fa --- /dev/null +++ b/src/main/resources/assets/anvilcraft/shaders/program/gravitational_lens.fsh @@ -0,0 +1,85 @@ +#version 150 + +uniform sampler2D DiffuseSampler; +uniform vec2 InSize; + +// BlackHole[i]: x=screenU, y=screenV, z=cameraDistance, w=lensDirection +// lensDirection > 0: convex (pull toward center, gravitational lens) +// lensDirection < 0: concave (push away from center, diverging lens) +layout (std140) uniform BlackHoles { + vec4 BlackHole[256]; +}; + +uniform float BlackHoleCount; +uniform float LensStrength; +uniform float EventHorizonRadius; +uniform float PerspectiveScale; + +in vec2 texCoord; +out vec4 fragColor; + +vec2 getHolePos(int i) { return BlackHole[i].xy; } +float getHoleDist(int i) { return BlackHole[i].z; } +float getLensDir(int i) { return BlackHole[i].w; } + +void main() { + vec2 uv = texCoord; + float aspectRatio = InSize.x / InSize.y; + + vec2 offset = vec2(0.0); + int count = int(BlackHoleCount); + + // --- Gravitational displacement --- + for (int i = 0; i < 256; i++) { + if (i >= count) break; + + vec2 holeUv = getHolePos(i); + vec2 toHole = holeUv - uv; + toHole.x *= aspectRatio; + float dist = length(toHole); + + if (dist < 0.0001) continue; + + vec2 dir = toHole / dist; + float perspScale = PerspectiveScale / max(getHoleDist(i), 0.1); + float lensDir = getLensDir(i); + + float gravity = LensStrength * perspScale / (dist * dist); + + vec2 lensOffset; + if (lensDir < 0.0) { + // Concave: push away from center, scaled by |lensDir| + lensOffset = -dir * gravity * (-lensDir); + } else { + // Convex: pull toward center + lensOffset = dir * gravity * lensDir; + } + lensOffset.x /= aspectRatio; + offset += lensOffset; + } + + vec3 color = texture(DiffuseSampler, uv + offset).rgb; + + // --- Render event horizon (convex, on-screen black holes only) --- + for (int i = 0; i < 256; i++) { + if (i >= count) break; + + // Concave lenses have no event horizon + if (getLensDir(i) <= 0.0) continue; + + vec2 holeUv = getHolePos(i); + + // Skip event horizon when the hole center is off-screen + if (holeUv.x < 0.0 || holeUv.x > 1.0 || holeUv.y < 0.0 || holeUv.y > 1.0) continue; + + float perspS = PerspectiveScale / max(getHoleDist(i), 0.1); + vec2 toHole = uv - holeUv; + toHole.x *= aspectRatio; + float dist = length(toHole); + float horizonMask = 1.0 - smoothstep(EventHorizonRadius * perspS * 0.95, EventHorizonRadius * perspS * 1.05, dist); + + color = mix(color, vec3(0.0, 0.0, 0.0), horizonMask); + } + + fragColor = vec4(color, 1.0); +} diff --git a/src/main/resources/assets/anvilcraft/shaders/program/gravitational_lens.json b/src/main/resources/assets/anvilcraft/shaders/program/gravitational_lens.json new file mode 100644 index 0000000000..2d55e4f446 --- /dev/null +++ b/src/main/resources/assets/anvilcraft/shaders/program/gravitational_lens.json @@ -0,0 +1,25 @@ +{ + "blend": { + "func": "add", + "srcrgb": "one", + "dstrgb": "zero" + }, + "vertex": "blit", + "fragment": "anvilcraft:gravitational_lens", + "attributes": [ + "Position" + ], + "samplers": [ + { "name": "DiffuseSampler" } + ], + "uniforms": [ + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "InSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "OutSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + + { "name": "BlackHoleCount", "type": "float", "count": 1, "values": [ 0.0 ] }, + { "name": "LensStrength", "type": "float", "count": 1, "values": [ 0.001953125 ] }, + { "name": "EventHorizonRadius", "type": "float", "count": 1, "values": [ 0.083333333 ] }, + { "name": "PerspectiveScale", "type": "float", "count": 1, "values": [ 10.0 ] } + ] +}