vanilla fluid rendering: water and lava as fluid polygons with level/neighbour corner heights, flow textures, double-sided and overlay faces, texture alpha, opaque lava, waterlogged blocks and seagrass/kelp/bubble columns hold water, underwater and lava fog from biome water_fog attributes with gaussian biome blend, fogged sky disc, 6/7 fov in fluids, sky light under water also for underwater cameras, fluid test scenes
This commit is contained in:
1 parent
9093c1ccfb
commit
4348b5fca0
8 files changed
+1160
-175
No files matched your search
@@ -27,11 +27,14 @@ public final class BlockModelRegistry {
|
||||
|
||||
private final Map<BlockData, ResolvedModel> cache = new ConcurrentHashMap<>();
|
||||
private final Map<ResourceLocation, ResolvedModel> modelCache = new ConcurrentHashMap<>();
|
||||
private volatile ResolvedModel waterModel;
|
||||
private volatile ResolvedModel lavaModel;
|
||||
|
||||
private final AssetReader reader;
|
||||
|
||||
/** The texture cache the models are baked from (also used for the fluid sprites). */
|
||||
public TextureCache textures() {
|
||||
return textures;
|
||||
}
|
||||
|
||||
/** The asset reader this registry resolves models with (shared by item-model resolution). */
|
||||
public AssetReader reader() {
|
||||
return reader;
|
||||
@@ -62,9 +65,9 @@ public final class BlockModelRegistry {
|
||||
|
||||
private ResolvedModel resolve(BlockData data) {
|
||||
Material material = data.getMaterial();
|
||||
if (material == Material.WATER) return water();
|
||||
if (material == Material.LAVA) return lava();
|
||||
// Technical blocks that are invisible in the world (their textures only show held in hand).
|
||||
// Technical blocks that are invisible in the world (their textures only show held in hand), and liquid
|
||||
// blocks (vanilla RenderShape.INVISIBLE: water, lava and bubble columns are drawn by the fluid renderer,
|
||||
// see render.raytrace.FluidRenderer; block displays of them show nothing, like vanilla).
|
||||
if (INVISIBLE_MATERIALS.contains(material)) {
|
||||
return new ResolvedModel(List.of(), 0, 0, 0, false, false);
|
||||
}
|
||||
@@ -107,7 +110,7 @@ public final class BlockModelRegistry {
|
||||
|
||||
private static EnumSet<Material> buildInvisibleMaterials() {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
for (String n : new String[] {"BARRIER", "LIGHT", "STRUCTURE_VOID"}) {
|
||||
for (String n : new String[] {"BARRIER", "LIGHT", "STRUCTURE_VOID", "WATER", "LAVA", "BUBBLE_COLUMN"}) {
|
||||
try {
|
||||
set.add(Material.valueOf(n));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
@@ -169,37 +172,4 @@ public final class BlockModelRegistry {
|
||||
}
|
||||
return 0xFF7F7F7F;
|
||||
}
|
||||
|
||||
private ResolvedModel water() {
|
||||
ResolvedModel m = waterModel;
|
||||
if (m == null) {
|
||||
m = liquid("block/water_still", 0, 0.60, 0.10);
|
||||
waterModel = m;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private ResolvedModel lava() {
|
||||
ResolvedModel m = lavaModel;
|
||||
if (m == null) {
|
||||
m = liquid("block/lava_still", -1, 0.15, 0.05);
|
||||
lavaModel = m;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Builds a simple full-cube model for a liquid texture with the given tint/transparency/reflection. */
|
||||
private ResolvedModel liquid(String texturePath, int tintIndex, double transparency, double reflection) {
|
||||
int[][] tex = textures.get(ResourceLocation.parse(texturePath)).orElse(null);
|
||||
if (tex == null) {
|
||||
return new ResolvedModel(List.of(solidCube(0xFF3F76E4)), 0xFF3F76E4, transparency, reflection, true, true);
|
||||
}
|
||||
Face[] faces = new Face[6];
|
||||
for (Direction d : Direction.values()) {
|
||||
faces[d.ordinal()] = new Face(tex, 0, 0, 1, 1, 0, tintIndex);
|
||||
}
|
||||
Element cube = new Element(new double[] {0, 0, 0}, new double[] {1, 1, 1}, faces, null, -1, 0, false);
|
||||
int avg = AverageColor.of(tex);
|
||||
return new ResolvedModel(List.of(cube), avg, transparency, reflection, true, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.raytrace;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.render.snapshot.WorldSnapshot;
|
||||
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeClimate;
|
||||
import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
/**
|
||||
* The fluid the camera is in and its fog, like vanilla 26.3 {@code FogRenderer} with {@code WaterFogEnvironment} /
|
||||
* {@code LavaFogEnvironment}: linear fog from {@code fogStart} to {@code fogEnd} (spherical distance from the
|
||||
* camera) towards {@code fogColor}, applied to every surface; the sky is the fogged sky disc.
|
||||
*
|
||||
* <p>Water: start/end/colour are the biome attributes {@code water_fog_start_distance} (-8),
|
||||
* {@code water_fog_end_distance} (96, swamps x0.85) and {@code water_fog_color} (default #050533), each blended
|
||||
* over the biomes around the camera with vanilla's {@code GaussianSampler} (6x6x6 quart cells, kernel 1-4-6-4-1).
|
||||
* The player's water vision is taken as fully adapted (1: after 30 s under water; {@code LocalPlayer
|
||||
* .getWaterVision}), which keeps the end distance and brightens the colour to a maximum channel of 1. Lava:
|
||||
* #991A00 from 0.25 to 1 block (no fire resistance).
|
||||
*/
|
||||
public record CameraMedium(int fluid, int fogColor, double fogStart, double fogEnd) {
|
||||
|
||||
public static final CameraMedium AIR = new CameraMedium(FluidRenderer.NONE, 0, 0, 0);
|
||||
|
||||
private static final double WATER_VISION = 1.0;
|
||||
private static final double[] KERNEL = {0, 1, 4, 6, 4, 1, 0};
|
||||
|
||||
// Vanilla sky disc (SkyRenderer.buildSkyDisc): a fan at height 16 around the camera, rim octagon at 512.
|
||||
private static final double DISC_HEIGHT = 16;
|
||||
private static final double DISC_RADIUS = 512;
|
||||
private static final double DISC_APOTHEM = DISC_RADIUS * Math.cos(Math.PI / 8);
|
||||
|
||||
public boolean inFluid() {
|
||||
return fluid != FluidRenderer.NONE;
|
||||
}
|
||||
|
||||
/** The medium at {@code eye} in {@code snapshot}. */
|
||||
public static CameraMedium at(WorldSnapshot snapshot, Vector eye, BiomeTintProvider tints) {
|
||||
double x = eye.getX(), y = eye.getY(), z = eye.getZ();
|
||||
int fluid = FluidRenderer.cameraFluid(snapshot::getBlockData, x, y, z);
|
||||
if (fluid == FluidRenderer.LAVA) return new CameraMedium(fluid, 0x991A00, 0.25, 1.0);
|
||||
if (fluid != FluidRenderer.WATER) return AIR;
|
||||
|
||||
// GaussianSampler.sample(position / 4) over the noise biomes (quart positions).
|
||||
double px = x * 0.25 - 0.5, py = y * 0.25 - 0.5, pz = z * 0.25 - 0.5;
|
||||
int ix = (int) Math.floor(px), iy = (int) Math.floor(py), iz = (int) Math.floor(pz);
|
||||
double rx = px - ix, ry = py - iy, rz = pz - iz;
|
||||
double r = 0, g = 0, b = 0, start = 0, end = 0, total = 0;
|
||||
for (int k = 0; k < 6; k++) {
|
||||
double wz = lerp(rz, KERNEL[k + 1], KERNEL[k]);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
double wx = lerp(rx, KERNEL[i + 1], KERNEL[i]);
|
||||
for (int j = 0; j < 6; j++) {
|
||||
double wy = lerp(ry, KERNEL[j + 1], KERNEL[j]);
|
||||
double w = wx * wy * wz;
|
||||
if (w == 0) continue;
|
||||
// Quart y clamped into the captured height range (vanilla clamps to the world height).
|
||||
int by = Math.clamp((iy - 2 + j) * 4, snapshot.minY(), snapshot.maxY() - 1);
|
||||
Biome biome = snapshot.getBiome((ix - 2 + i) * 4, by, (iz - 2 + k) * 4);
|
||||
BiomeClimate.Climate c = biome == null ? BiomeClimate.DEFAULT : tints.climate(biome);
|
||||
int fog = c.waterFogColor();
|
||||
r += w * ((fog >> 16) & 0xFF) / 255.0;
|
||||
g += w * ((fog >> 8) & 0xFF) / 255.0;
|
||||
b += w * (fog & 0xFF) / 255.0;
|
||||
start += w * c.waterFogStart();
|
||||
end += w * c.waterFogEnd();
|
||||
total += w;
|
||||
}
|
||||
}
|
||||
}
|
||||
r /= total;
|
||||
g /= total;
|
||||
b /= total;
|
||||
start /= total;
|
||||
end /= total;
|
||||
// FogRenderer.computeFogColor: brighten by water vision towards a maximum channel of 1.
|
||||
if (r != 0 && g != 0 && b != 0) {
|
||||
double scale = 1 / Math.max(r, Math.max(g, b));
|
||||
r = r + (r * scale - r) * WATER_VISION;
|
||||
g = g + (g * scale - g) * WATER_VISION;
|
||||
b = b + (b * scale - b) * WATER_VISION;
|
||||
}
|
||||
end *= Math.max(0.25, WATER_VISION);
|
||||
int color = ColorUtil.argb(255, channel(r), channel(g), channel(b)) & 0xFFFFFF;
|
||||
return new CameraMedium(fluid, color, start, end);
|
||||
}
|
||||
|
||||
private static double lerp(double a, double from, double to) {
|
||||
return from + a * (to - from);
|
||||
}
|
||||
|
||||
private static int channel(double v) {
|
||||
return Math.clamp((int) Math.round(v * 255), 0, 255);
|
||||
}
|
||||
|
||||
/** fog.glsl linear_fog_value at distance {@code d}. */
|
||||
public double fogValue(double d) {
|
||||
if (d <= fogStart) return 0;
|
||||
if (d >= fogEnd) return 1;
|
||||
return (d - fogStart) / (fogEnd - fogStart);
|
||||
}
|
||||
|
||||
/** {@code color} (RGB) fogged at distance {@code d}. */
|
||||
public int fog(int color, double d) {
|
||||
double f = fogValue(d);
|
||||
return f > 0 ? ColorUtil.mix(color, fogColor, 1 - f, f) : color & 0xFFFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sky seen in direction {@code (dx, dy, dz)} (unit) from inside the fluid: vanilla draws the sky disc with
|
||||
* fog over {@code [0, skyEnd = fogEnd]} using the vertex distances interpolated across the fan triangles
|
||||
* (centre 16, rim 512, plus the cylindrical distance beyond skyEnd = fully fogged); below the horizon and
|
||||
* beyond the disc only the clear colour (= fog colour) remains.
|
||||
*/
|
||||
public int sky(int skyColor, double dx, double dy, double dz) {
|
||||
if (fluid == FluidRenderer.LAVA || dy <= 1e-9) return fogColor;
|
||||
double horizontal = Math.sqrt(dx * dx + dz * dz);
|
||||
double radius = DISC_HEIGHT * horizontal / dy;
|
||||
double lambda = 0;
|
||||
if (radius > 0) {
|
||||
double phi = Math.atan2(dz, dx);
|
||||
double sector = Math.PI / 4;
|
||||
double rel = phi - Math.floor(phi / sector) * sector; // 0..45°
|
||||
lambda = radius * Math.cos(rel - sector / 2) / DISC_APOTHEM;
|
||||
}
|
||||
if (lambda >= 1) return fogColor;
|
||||
double spherical = DISC_HEIGHT + (Math.hypot(DISC_RADIUS, DISC_HEIGHT) - DISC_HEIGHT) * lambda;
|
||||
double cylindrical = DISC_HEIGHT + (DISC_RADIUS - DISC_HEIGHT) * lambda;
|
||||
double f = cylindrical > fogEnd ? 1 : Math.clamp(spherical / fogEnd, 0, 1);
|
||||
return ColorUtil.mix(skyColor, fogColor, 1 - f, f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
package eu.mhsl.minecraft.pixelpics.render.raytrace;
|
||||
|
||||
import eu.mhsl.minecraft.pixelpics.assets.BlockModelRegistry;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.ResourceLocation;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.TextureCache;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Direction;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Element;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.Face;
|
||||
import eu.mhsl.minecraft.pixelpics.assets.model.ResolvedModel;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.BlockSupport;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.block.data.Levelled;
|
||||
import org.bukkit.block.data.Waterlogged;
|
||||
|
||||
/**
|
||||
* Water and lava like vanilla 26.3 {@code FluidRenderer} (decompiled client): fluids are not block models but
|
||||
* polygons built per cell from the fluid state and its neighbours: a top surface whose four corner heights come
|
||||
* from the cell's level (amount / 9, source 8/9, full when the same fluid lies above) averaged with the
|
||||
* neighbours ({@code calculateAverageHeight}, split into the two triangles NW-SW-SE / NW-SE-NE), side faces
|
||||
* against every neighbour that is not the same fluid and does not cover the face, and a bottom face. Top and
|
||||
* side faces are double-sided (the top only when {@code shouldRenderBackwardUpFace}); side faces against
|
||||
* {@code HalfTransparentBlock}/{@code LeavesBlock} use the water overlay texture and are one-sided; the bottom is
|
||||
* one-sided (seen from below). Waterlogged blocks and seagrass/kelp/bubble columns hold a water source.
|
||||
*
|
||||
* <p>The tracer asks for the nearest drawn fluid polygon a ray crosses inside one cell ({@link #nearest}); the
|
||||
* caller tints/lights it and blends it as a translucent layer (water texture alpha 180) or ends the ray on it
|
||||
* (lava, opaque).
|
||||
*/
|
||||
public final class FluidRenderer {
|
||||
|
||||
public static final int NONE = 0;
|
||||
public static final int WATER = 1;
|
||||
public static final int LAVA = 2;
|
||||
|
||||
// Vanilla FluidRenderer insets: faces 0.001 inside the cell, the top 0.001 below the computed height.
|
||||
private static final float OFFS = 0.001F;
|
||||
private static final double EPS = 1e-9;
|
||||
|
||||
/** Block access by world position: a {@code WorldSnapshot} or the live world (main thread). */
|
||||
@FunctionalInterface
|
||||
public interface BlockLookup {
|
||||
BlockData at(int x, int y, int z);
|
||||
}
|
||||
|
||||
/** Blocks whose fluid state is always a water source (LiquidBlockContainer without a waterlogged property). */
|
||||
private static final EnumSet<Material> ALWAYS_WATER =
|
||||
materials("SEAGRASS", "TALL_SEAGRASS", "KELP", "KELP_PLANT", "BUBBLE_COLUMN");
|
||||
|
||||
// Vanilla HalfTransparentBlock / LeavesBlock: water draws its side faces against them with the overlay texture,
|
||||
// one-sided (FluidRenderer isOverlay). Blue ice is one too but occludes (its face is culled anyway).
|
||||
private static final EnumSet<Material> OVERLAY_NEIGHBOURS = buildOverlayNeighbours();
|
||||
|
||||
private static EnumSet<Material> materials(String... names) {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
for (String n : names) {
|
||||
try {
|
||||
set.add(Material.valueOf(n));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
/* older/newer server */
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static EnumSet<Material> buildOverlayNeighbours() {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
for (Material m : Material.values()) {
|
||||
if (m.isLegacy()) continue;
|
||||
String n = m.name();
|
||||
if (n.equals("GLASS")
|
||||
|| n.equals("TINTED_GLASS")
|
||||
|| n.endsWith("_STAINED_GLASS")
|
||||
|| n.equals("ICE")
|
||||
|| n.equals("FROSTED_ICE")
|
||||
|| n.equals("BLUE_ICE")
|
||||
|| n.equals("SLIME_BLOCK")
|
||||
|| n.equals("HONEY_BLOCK")
|
||||
|| n.endsWith("_LEAVES")) set.add(m);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
// --- fluid state: type (bits 0-1), amount 1..8 (bits 2-5), falling (bit 6), source (bit 7) -------------
|
||||
|
||||
private static final int WATER_SOURCE = WATER | 8 << 2 | 1 << 7;
|
||||
|
||||
/** The fluid state of a block (vanilla {@code BlockState.getFluidState}), packed; 0 = no fluid. */
|
||||
public static int state(BlockData data) {
|
||||
Material m = data.getMaterial();
|
||||
if (m == Material.AIR) return 0;
|
||||
if (m == Material.WATER || m == Material.LAVA) {
|
||||
int type = m == Material.WATER ? WATER : LAVA;
|
||||
// LiquidBlock: level 0 = source, 1..7 = flowing amount 8 - level, 8..15 = falling amount 8.
|
||||
int level = data instanceof Levelled l ? l.getLevel() : 0;
|
||||
if (level == 0) return type | 8 << 2 | 1 << 7;
|
||||
if (level >= 8) return type | 8 << 2 | 1 << 6;
|
||||
return type | (8 - level) << 2;
|
||||
}
|
||||
if (ALWAYS_WATER.contains(m)) return WATER_SOURCE;
|
||||
if (data instanceof Waterlogged w && w.isWaterlogged()) return WATER_SOURCE;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static int type(int state) {
|
||||
return state & 3;
|
||||
}
|
||||
|
||||
static int amount(int state) {
|
||||
return (state >> 2) & 15;
|
||||
}
|
||||
|
||||
static boolean source(int state) {
|
||||
return (state & 1 << 7) != 0;
|
||||
}
|
||||
|
||||
/** FlowingFluid.getOwnHeight: amount / 9 (0 without fluid). */
|
||||
static float ownHeight(int state) {
|
||||
return amount(state) / 9.0F;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which fluid the camera at {@code (x, y, z)} is in (vanilla {@code Camera.getFluidInCamera}): the eye block's
|
||||
* fluid, when the eye is below its camera height ({@code FluidState.getHeightForCamera}: 1 for a source under a
|
||||
* block with a sturdy bottom face or with the same fluid above, else amount / 9).
|
||||
*/
|
||||
public static int cameraFluid(BlockLookup level, double x, double y, double z) {
|
||||
int bx = (int) Math.floor(x), by = (int) Math.floor(y), bz = (int) Math.floor(z);
|
||||
int s = state(level.at(bx, by, bz));
|
||||
int type = type(s);
|
||||
if (type == NONE) return NONE;
|
||||
BlockData above = level.at(bx, by + 1, bz);
|
||||
float h;
|
||||
if (type(state(above)) == type) {
|
||||
h = 1;
|
||||
} else if (source(s) && sturdyBottom(above)) {
|
||||
h = 1;
|
||||
} else {
|
||||
h = ownHeight(s);
|
||||
}
|
||||
double rel = y - by;
|
||||
// Water: strictly below the surface; lava (near-plane check): at or below it.
|
||||
return type == WATER ? (rel < h ? WATER : NONE) : (rel <= h ? LAVA : NONE);
|
||||
}
|
||||
|
||||
private static boolean sturdyBottom(BlockData data) {
|
||||
if (data.getMaterial().isAir()) return false;
|
||||
try {
|
||||
return data.isFaceSturdy(BlockFace.DOWN, BlockSupport.FULL);
|
||||
} catch (RuntimeException e) {
|
||||
return data.getMaterial().isOccluding();
|
||||
}
|
||||
}
|
||||
|
||||
// --- sky light under water ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Vanilla sky light level of the cell (x, y, z) when water lies above it: 15 minus one level per block from
|
||||
* this cell up to the topmost water block of the column (water costs one level, non-occluding blocks below the
|
||||
* surface such as glass at least one). Cells without water above them (in the next 15 blocks) get 15.
|
||||
*/
|
||||
public static int skyLevel(BlockLookup level, int x, int y, int z) {
|
||||
int water = 0;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
BlockData d = level.at(x, y + i, z);
|
||||
if (type(state(d)) == WATER) {
|
||||
water = i + 1;
|
||||
continue;
|
||||
}
|
||||
Material m = d.getMaterial();
|
||||
if (m.isAir() || m.isOccluding()) break;
|
||||
}
|
||||
return 15 - water;
|
||||
}
|
||||
|
||||
// --- textures ------------------------------------------------------------------------------------------
|
||||
|
||||
private final BlockModelRegistry registry;
|
||||
private final int[][] waterStill, waterFlow, waterOverlay, lavaStill, lavaFlow;
|
||||
// Bit per Direction: the block model covers that whole cell face with opaque texels (vanilla face occlusion
|
||||
// shape of blocks that can occlude). Identity keys (ResolvedModel is cached per BlockData).
|
||||
private final Map<ResolvedModel, Integer> fullFaces = new ConcurrentHashMap<>();
|
||||
|
||||
public FluidRenderer(BlockModelRegistry registry, TextureCache textures) {
|
||||
this.registry = registry;
|
||||
this.waterStill = texture(textures, "block/water_still", 0xB4B1B1B1);
|
||||
this.waterFlow = texture(textures, "block/water_flow", 0xB4AAAAAA);
|
||||
this.waterOverlay = texture(textures, "block/water_overlay", 0xB4A5A5A5);
|
||||
this.lavaStill = texture(textures, "block/lava_still", 0xFFD9691A);
|
||||
this.lavaFlow = texture(textures, "block/lava_flow", 0xFFCF5C15);
|
||||
}
|
||||
|
||||
private static int[][] texture(TextureCache textures, String path, int fallback) {
|
||||
int[][] tex = textures.get(ResourceLocation.parse(path)).orElse(null);
|
||||
return tex != null && tex.length > 0 && tex[0].length > 0 ? tex : new int[][] {{fallback}};
|
||||
}
|
||||
|
||||
/** Samples a sprite at (u, v) in 0..1 of the sprite (vanilla getU/getV), nearest texel. */
|
||||
private static int sample(int[][] tex, double u, double v) {
|
||||
int h = tex.length, w = tex[0].length;
|
||||
int x = Math.clamp((int) Math.floor(u * w), 0, w - 1);
|
||||
int y = Math.clamp((int) Math.floor(v * h), 0, h - 1);
|
||||
return tex[y][x];
|
||||
}
|
||||
|
||||
// --- face occlusion ------------------------------------------------------------------------------------
|
||||
|
||||
private int fullFaces(BlockData data) {
|
||||
Material m = data.getMaterial();
|
||||
if (m.isAir()) return 0;
|
||||
return fullFaces.computeIfAbsent(registry.get(data), FluidRenderer::computeFullFaces);
|
||||
}
|
||||
|
||||
/** Faces fully covered by opaque, unrotated element faces on the cell boundary (8x8 sample grid). */
|
||||
private static int computeFullFaces(ResolvedModel model) {
|
||||
int mask = 0;
|
||||
if (model.elements.isEmpty()) return 0;
|
||||
for (Direction d : Direction.values()) {
|
||||
int axis = d.nx != 0 ? 0 : d.ny != 0 ? 1 : 2;
|
||||
int a1 = axis == 0 ? 1 : 0, a2 = axis == 2 ? 1 : 2;
|
||||
boolean positive = d.nx + d.ny + d.nz > 0;
|
||||
boolean covered = true;
|
||||
for (int i = 0; i < 8 && covered; i++) {
|
||||
for (int j = 0; j < 8 && covered; j++) {
|
||||
double p1 = (i + 0.5) / 8, p2 = (j + 0.5) / 8;
|
||||
boolean any = false;
|
||||
for (Element e : model.elements) {
|
||||
if (e.rotAxis != -1) continue;
|
||||
Face f = e.faces[d.ordinal()];
|
||||
if (f == null || !opaqueTexture(f)) continue;
|
||||
double bound = positive ? e.to[axis] : e.from[axis];
|
||||
if (Math.abs(bound - (positive ? 1 : 0)) > 1e-6) continue;
|
||||
if (p1 < Math.min(e.from[a1], e.to[a1]) || p1 > Math.max(e.from[a1], e.to[a1])) continue;
|
||||
if (p2 < Math.min(e.from[a2], e.to[a2]) || p2 > Math.max(e.from[a2], e.to[a2])) continue;
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
covered = any;
|
||||
}
|
||||
}
|
||||
if (covered) mask |= 1 << d.ordinal();
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
private static boolean opaqueTexture(Face f) {
|
||||
if (f.texture == null) return false;
|
||||
for (int[] row : f.texture) for (int px : row) if ((px >>> 24) != 0xFF) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** FluidRenderer.isFaceOccludedByState for a neighbour/self block whose face points at the fluid. */
|
||||
private boolean occludes(BlockData block, Direction face) {
|
||||
return (fullFaces(block) & 1 << face.ordinal()) != 0;
|
||||
}
|
||||
|
||||
// --- heights -------------------------------------------------------------------------------------------
|
||||
|
||||
/** FluidRenderer.getHeight: 1 under the same fluid, own height in it, 0 next to air etc., -1 next to solids. */
|
||||
private static float height(BlockLookup level, int type, int x, int y, int z, BlockData data, int state) {
|
||||
if (type(state) == type) {
|
||||
return type(state(level.at(x, y + 1, z))) == type ? 1.0F : ownHeight(state);
|
||||
}
|
||||
return !data.getMaterial().isSolid() ? 0.0F : -1.0F;
|
||||
}
|
||||
|
||||
private static float height(BlockLookup level, int type, int x, int y, int z) {
|
||||
BlockData d = level.at(x, y, z);
|
||||
return height(level, type, x, y, z, d, state(d));
|
||||
}
|
||||
|
||||
private static float averageHeight(
|
||||
BlockLookup level, int type, float self, float h2, float h1, int cx, int cy, int cz) {
|
||||
if (h1 >= 1.0F || h2 >= 1.0F) return 1.0F;
|
||||
float[] w = new float[2];
|
||||
if (h1 > 0.0F || h2 > 0.0F) {
|
||||
float corner = height(level, type, cx, cy, cz);
|
||||
if (corner >= 1.0F) return 1.0F;
|
||||
addWeighted(w, corner);
|
||||
}
|
||||
addWeighted(w, self);
|
||||
addWeighted(w, h1);
|
||||
addWeighted(w, h2);
|
||||
return w[0] / w[1];
|
||||
}
|
||||
|
||||
private static void addWeighted(float[] w, float h) {
|
||||
if (h >= 0.8F) {
|
||||
w[0] += h * 10.0F;
|
||||
w[1] += 10.0F;
|
||||
} else if (h >= 0.0F) {
|
||||
w[0] += h;
|
||||
w[1]++;
|
||||
}
|
||||
}
|
||||
|
||||
// --- flow (top texture direction) ---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* FlowingFluid.getFlow, horizontal part ({x, z}, not normalised; the falling tweak only adds a y component,
|
||||
* which does not change the texture angle). {0, 0} = still texture.
|
||||
*/
|
||||
private static double[] flow(BlockLookup level, int type, int x, int y, int z, int state) {
|
||||
double fx = 0, fz = 0;
|
||||
float own = ownHeight(state);
|
||||
int[][] dirs = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
|
||||
for (int[] d : dirs) {
|
||||
BlockData nd = level.at(x + d[0], y, z + d[1]);
|
||||
int ns = state(nd);
|
||||
if (!affectsFlow(type, ns)) continue;
|
||||
float nh = ownHeight(ns);
|
||||
float distance = 0.0F;
|
||||
if (nh == 0.0F) {
|
||||
if (!blocksFluidFlow(nd)) {
|
||||
int below = state(level.at(x + d[0], y - 1, z + d[1]));
|
||||
if (affectsFlow(type, below)) {
|
||||
nh = ownHeight(below);
|
||||
if (nh > 0.0F) distance = own - (nh - 0.8888889F);
|
||||
}
|
||||
}
|
||||
} else if (nh > 0.0F) {
|
||||
distance = own - nh;
|
||||
}
|
||||
if (distance != 0.0F) {
|
||||
fx += d[0] * distance;
|
||||
fz += d[1] * distance;
|
||||
}
|
||||
}
|
||||
// Vec3.normalize: below 1e-5 the vector is zero (still texture).
|
||||
if (Math.sqrt(fx * fx + fz * fz) < 1.0E-5) return new double[] {0, 0};
|
||||
return new double[] {fx, fz};
|
||||
}
|
||||
|
||||
private static boolean affectsFlow(int type, int neighbourState) {
|
||||
int t = type(neighbourState);
|
||||
return t == NONE || t == type;
|
||||
}
|
||||
|
||||
/** Vanilla tag blocks_fluid_flow (= #blocks_motion + #all_signs). */
|
||||
private static boolean blocksFluidFlow(BlockData data) {
|
||||
Material m = data.getMaterial();
|
||||
return m.isSolid() || m.name().endsWith("_SIGN");
|
||||
}
|
||||
|
||||
/** FluidState.shouldRenderBackwardUpFace: some block of the 3x3 above is neither the fluid nor solid-render. */
|
||||
private static boolean backwardUpFace(BlockLookup level, int type, int x, int y, int z) {
|
||||
for (int ox = -1; ox <= 1; ox++) {
|
||||
for (int oz = -1; oz <= 1; oz++) {
|
||||
BlockData d = level.at(x + ox, y, z + oz);
|
||||
if (type(state(d)) != type && !d.getMaterial().isOccluding()) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- ray crossing --------------------------------------------------------------------------------------
|
||||
|
||||
/** A drawn fluid polygon crossed by a ray: texel (ARGB), shade, face, outward normal and side. */
|
||||
public static final class Crossing {
|
||||
public double t;
|
||||
public int texel;
|
||||
public double shade;
|
||||
public int type;
|
||||
public boolean entering; // the ray comes from outside the fluid (front side)
|
||||
public double nx, ny, nz; // outward normal (unit)
|
||||
public int lightX, lightY, lightZ; // cell whose light (max with the one above) lights the face
|
||||
}
|
||||
|
||||
/**
|
||||
* The nearest fluid polygon of cell (bx, by, bz) (holding {@code data} with fluid {@code state}) crossed by
|
||||
* the ray {@code o + t·d} with {@code t > minT}, or null.
|
||||
*/
|
||||
public Crossing nearest(
|
||||
BlockLookup level,
|
||||
int bx,
|
||||
int by,
|
||||
int bz,
|
||||
BlockData data,
|
||||
int state,
|
||||
double ox,
|
||||
double oy,
|
||||
double oz,
|
||||
double dx,
|
||||
double dy,
|
||||
double dz,
|
||||
double minT) {
|
||||
int type = type(state);
|
||||
BlockData down = level.at(bx, by - 1, bz), up = level.at(bx, by + 1, bz);
|
||||
BlockData north = level.at(bx, by, bz - 1), south = level.at(bx, by, bz + 1);
|
||||
BlockData west = level.at(bx - 1, by, bz), east = level.at(bx + 1, by, bz);
|
||||
int sDown = state(down), sUp = state(up), sNorth = state(north), sSouth = state(south);
|
||||
int sWest = state(west), sEast = state(east);
|
||||
boolean sameUp = type(sUp) == type, sameDown = type(sDown) == type;
|
||||
boolean sameN = type(sNorth) == type, sameS = type(sSouth) == type;
|
||||
boolean sameW = type(sWest) == type, sameE = type(sEast) == type;
|
||||
if (sameUp && sameDown && sameN && sameS && sameW && sameE) return null; // inside a body of fluid
|
||||
|
||||
boolean renderUp = !sameUp;
|
||||
boolean renderDown = !sameDown && !occludes(data, Direction.DOWN) && !occludes(down, Direction.UP);
|
||||
boolean renderN = !sameN && !occludes(data, Direction.NORTH);
|
||||
boolean renderS = !sameS && !occludes(data, Direction.SOUTH);
|
||||
boolean renderW = !sameW && !occludes(data, Direction.WEST);
|
||||
boolean renderE = !sameE && !occludes(data, Direction.EAST);
|
||||
if (!(renderUp || renderDown || renderN || renderS || renderW || renderE)) return null;
|
||||
|
||||
float self = height(level, type, bx, by, bz, data, state);
|
||||
float hNE, hNW, hSE, hSW;
|
||||
if (self >= 1.0F) {
|
||||
hNE = hNW = hSE = hSW = 1.0F;
|
||||
} else {
|
||||
float hN = height(level, type, bx, by, bz - 1, north, sNorth);
|
||||
float hS = height(level, type, bx, by, bz + 1, south, sSouth);
|
||||
float hE = height(level, type, bx + 1, by, bz, east, sEast);
|
||||
float hW = height(level, type, bx - 1, by, bz, west, sWest);
|
||||
hNE = averageHeight(level, type, self, hN, hE, bx + 1, by, bz - 1);
|
||||
hNW = averageHeight(level, type, self, hN, hW, bx - 1, by, bz - 1);
|
||||
hSE = averageHeight(level, type, self, hS, hE, bx + 1, by, bz + 1);
|
||||
hSW = averageHeight(level, type, self, hS, hW, bx - 1, by, bz + 1);
|
||||
}
|
||||
float bottom = renderDown ? OFFS : 0.0F;
|
||||
// The top is not drawn under a block covering it when every corner is full.
|
||||
boolean drawUp =
|
||||
renderUp && !(Math.min(Math.min(hNW, hSW), Math.min(hSE, hNE)) >= 1.0F && occludes(up, Direction.DOWN));
|
||||
if (drawUp) {
|
||||
hNW -= OFFS;
|
||||
hSW -= OFFS;
|
||||
hSE -= OFFS;
|
||||
hNE -= OFFS;
|
||||
}
|
||||
|
||||
double lx = ox - bx, ly = oy - by, lz = oz - bz;
|
||||
double bestT = Double.POSITIVE_INFINITY;
|
||||
int bestFace = -1; // 0 top, 1 bottom, 2 N, 3 S, 4 W, 5 E
|
||||
double bestNx = 0, bestNy = 0, bestNz = 0;
|
||||
boolean bestEntering = false;
|
||||
double hitX = 0, hitY = 0, hitZ = 0;
|
||||
boolean sideOverlay = false;
|
||||
double sideS = 0;
|
||||
|
||||
if (drawUp) {
|
||||
Boolean backUp = null;
|
||||
// Triangle 1: NW(0,0) SW(0,1) SE(1,1) (z >= x); triangle 2: NW, SE, NE(1,0) (z < x).
|
||||
for (int tri = 0; tri < 2; tri++) {
|
||||
double a = hNW, b, c;
|
||||
if (tri == 0) {
|
||||
b = hSE - hSW;
|
||||
c = hSW - hNW;
|
||||
} else {
|
||||
b = hNE - hNW;
|
||||
c = hSE - hNE;
|
||||
}
|
||||
// Plane y = a + b·x + c·z, outward normal (-b, 1, -c).
|
||||
double denom = dy - b * dx - c * dz;
|
||||
if (Math.abs(denom) < 1e-12) continue;
|
||||
double t = (a + b * lx + c * lz - ly) / denom;
|
||||
if (!(t > minT) || t >= bestT) continue;
|
||||
double x = lx + dx * t, z = lz + dz * t;
|
||||
if (x < -EPS || x > 1 + EPS || z < -EPS || z > 1 + EPS) continue;
|
||||
if (tri == 0 ? z < x - EPS : z >= x + EPS) continue;
|
||||
boolean entering = denom < 0; // dot(d, n) = dy - b·dx - c·dz
|
||||
if (!entering) {
|
||||
if (backUp == null) backUp = backwardUpFace(level, type, bx, by + 1, bz);
|
||||
if (!backUp) continue;
|
||||
}
|
||||
double len = Math.sqrt(b * b + 1 + c * c);
|
||||
bestT = t;
|
||||
bestFace = 0;
|
||||
bestNx = -b / len;
|
||||
bestNy = 1 / len;
|
||||
bestNz = -c / len;
|
||||
bestEntering = entering;
|
||||
hitX = x;
|
||||
hitY = ly + dy * t;
|
||||
hitZ = z;
|
||||
}
|
||||
}
|
||||
if (renderDown && dy > 0) {
|
||||
// One-sided, seen from below only.
|
||||
double t = (bottom - ly) / dy;
|
||||
if (t > minT && t < bestT) {
|
||||
double x = lx + dx * t, z = lz + dz * t;
|
||||
if (x >= -EPS && x <= 1 + EPS && z >= -EPS && z <= 1 + EPS) {
|
||||
bestT = t;
|
||||
bestFace = 1;
|
||||
bestNx = 0;
|
||||
bestNy = -1;
|
||||
bestNz = 0;
|
||||
bestEntering = true;
|
||||
hitX = x;
|
||||
hitY = bottom;
|
||||
hitZ = z;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Side faces: plane, outward normal, edge heights from vertex 0 to vertex 1 (vanilla order) along s.
|
||||
for (int face = 2; face <= 5; face++) {
|
||||
boolean render;
|
||||
BlockData neighbour;
|
||||
double plane;
|
||||
float h0, h1;
|
||||
switch (face) {
|
||||
case 2 -> {
|
||||
render = renderN;
|
||||
neighbour = north;
|
||||
plane = OFFS;
|
||||
h0 = hNW;
|
||||
h1 = hNE;
|
||||
}
|
||||
case 3 -> {
|
||||
render = renderS;
|
||||
neighbour = south;
|
||||
plane = 1 - OFFS;
|
||||
h0 = hSE;
|
||||
h1 = hSW;
|
||||
}
|
||||
case 4 -> {
|
||||
render = renderW;
|
||||
neighbour = west;
|
||||
plane = OFFS;
|
||||
h0 = hSW;
|
||||
h1 = hNW;
|
||||
}
|
||||
default -> {
|
||||
render = renderE;
|
||||
neighbour = east;
|
||||
plane = 1 - OFFS;
|
||||
h0 = hNE;
|
||||
h1 = hSE;
|
||||
}
|
||||
}
|
||||
if (!render) continue;
|
||||
boolean alongX = face == 2 || face == 3;
|
||||
double dAxis = alongX ? dz : dx;
|
||||
if (Math.abs(dAxis) < 1e-12) continue;
|
||||
double t = (plane - (alongX ? lz : lx)) / dAxis;
|
||||
if (!(t > minT) || t >= bestT) continue;
|
||||
double outward = face == 2 || face == 4 ? -1 : 1;
|
||||
boolean entering = dAxis * outward < 0;
|
||||
Direction toNeighbour =
|
||||
switch (face) {
|
||||
case 2 -> Direction.NORTH;
|
||||
case 3 -> Direction.SOUTH;
|
||||
case 4 -> Direction.WEST;
|
||||
default -> Direction.EAST;
|
||||
};
|
||||
// Covered by the neighbour's face towards the fluid (full block face; partial shapes never cover the
|
||||
// whole fluid face here).
|
||||
if (occludes(neighbour, opposite(toNeighbour))) continue;
|
||||
boolean overlay = type == WATER && OVERLAY_NEIGHBOURS.contains(neighbour.getMaterial());
|
||||
if (!entering && overlay) continue;
|
||||
double x = lx + dx * t, y = ly + dy * t, z = lz + dz * t;
|
||||
double along = alongX ? x : z;
|
||||
if (along < -EPS || along > 1 + EPS) continue;
|
||||
// s: position from vertex 0 to vertex 1 (N: west->east, S: east->west, W: south->north, E: north->south)
|
||||
double s =
|
||||
switch (face) {
|
||||
case 2 -> x;
|
||||
case 3 -> 1 - x;
|
||||
case 4 -> 1 - z;
|
||||
default -> z;
|
||||
};
|
||||
double top = h0 + (h1 - h0) * s;
|
||||
if (y < bottom - EPS || y > top + EPS) continue;
|
||||
bestT = t;
|
||||
bestFace = face;
|
||||
bestNx = alongX ? 0 : outward;
|
||||
bestNy = 0;
|
||||
bestNz = alongX ? outward : 0;
|
||||
bestEntering = entering;
|
||||
hitX = x;
|
||||
hitY = y;
|
||||
hitZ = z;
|
||||
sideOverlay = overlay;
|
||||
sideS = s;
|
||||
}
|
||||
if (bestFace < 0) return null;
|
||||
|
||||
Crossing best = new Crossing();
|
||||
best.t = bestT;
|
||||
best.type = type;
|
||||
best.entering = bestEntering;
|
||||
best.nx = bestNx;
|
||||
best.ny = bestNy;
|
||||
best.nz = bestNz;
|
||||
best.lightX = bx;
|
||||
best.lightY = bestFace == 1 ? by - 1 : by;
|
||||
best.lightZ = bz;
|
||||
boolean water = type == WATER;
|
||||
switch (bestFace) {
|
||||
case 0 -> {
|
||||
double[] f = flow(level, type, bx, by, bz, state);
|
||||
if (f[0] == 0 && f[1] == 0) {
|
||||
best.texel = sample(water ? waterStill : lavaStill, hitX, hitZ);
|
||||
} else {
|
||||
// Flowing sprite turned to the flow (vanilla UVs at the corners; affine over the cell).
|
||||
float angle = (float) Math.atan2(f[1], f[0]) - (float) (Math.PI / 2);
|
||||
double sn = Math.sin(angle) * 0.25F, cs = Math.cos(angle) * 0.25F;
|
||||
double u = 0.5 - cs - sn + 2 * cs * hitX + 2 * sn * hitZ;
|
||||
double v = 0.5 - cs + sn - 2 * sn * hitX + 2 * cs * hitZ;
|
||||
best.texel = sample(water ? waterFlow : lavaFlow, u, v);
|
||||
}
|
||||
best.shade = 1.0;
|
||||
}
|
||||
case 1 -> {
|
||||
best.texel = sample(water ? waterStill : lavaStill, hitX, hitZ);
|
||||
best.shade = 0.5;
|
||||
}
|
||||
default -> {
|
||||
int[][] sprite = sideOverlay ? waterOverlay : water ? waterFlow : lavaFlow;
|
||||
best.texel = sample(sprite, sideS * 0.5, (1 - hitY) * 0.5);
|
||||
best.shade = bestFace <= 3 ? 0.8 : 0.6;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static Direction opposite(Direction d) {
|
||||
return switch (d) {
|
||||
case NORTH -> Direction.SOUTH;
|
||||
case SOUTH -> Direction.NORTH;
|
||||
case WEST -> Direction.EAST;
|
||||
case EAST -> Direction.WEST;
|
||||
case UP -> Direction.DOWN;
|
||||
case DOWN -> Direction.UP;
|
||||
};
|
||||
}
|
||||
}
|
||||
+135
-129
@@ -11,7 +11,6 @@ import eu.mhsl.minecraft.pixelpics.render.tint.BiomeTintProvider;
|
||||
import eu.mhsl.minecraft.pixelpics.render.tint.TintResolver;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.ColorUtil;
|
||||
import eu.mhsl.minecraft.pixelpics.render.util.MathUtil;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.bukkit.Material;
|
||||
@@ -46,11 +45,14 @@ public final class SnapshotRaytracer {
|
||||
// layerAlpha marker of an additive (emissive) layer: its colour is added to what lies behind.
|
||||
private static final double ADDITIVE = -1;
|
||||
|
||||
// Brightness of a surface under water seen from above, by sky light level (index): vanilla lightmap
|
||||
// (lightmap.fsh: level / (4 - 3·level), mixed halfway with notGamma = default brightness option 0.5, overworld
|
||||
// ambient 0). Sky light loses one level per water block above the face (water's light block = 1), so the floor
|
||||
// gets darker with VERTICAL depth; the path length through the water plays no role in vanilla when the camera
|
||||
// is above the surface (no water fog there): one translucent surface layer over the dimmed scene.
|
||||
// Water reflection (not vanilla; kept as before): weight of the reflected ray on the first water surface hit
|
||||
// from above (rays starting in the air only).
|
||||
private static final double WATER_REFLECTION = 0.10;
|
||||
|
||||
// Brightness by sky light level (index): vanilla lightmap (lightmap.fsh: level / (4 - 3·level), mixed halfway
|
||||
// with notGamma = default brightness option 0.5, overworld ambient 0). Sky light loses one level per water
|
||||
// block (water's light block = 1), so surfaces under water get darker with VERTICAL depth (FluidRenderer
|
||||
// .skyLevel); PixelPics has no other light model, so this only applies under water.
|
||||
private static final double[] SKY_LIGHT = new double[16];
|
||||
|
||||
static {
|
||||
@@ -62,28 +64,8 @@ public final class SnapshotRaytracer {
|
||||
}
|
||||
}
|
||||
|
||||
// Vanilla HalfTransparentBlock / LeavesBlock that do not occlude: water renders its faces against them with the
|
||||
// water overlay texture (FluidRenderer). Blue ice is a HalfTransparentBlock too but occludes, packed ice is not.
|
||||
private static final EnumSet<Material> WATER_OVERLAY_NEIGHBOURS = buildWaterOverlayNeighbours();
|
||||
|
||||
private static EnumSet<Material> buildWaterOverlayNeighbours() {
|
||||
EnumSet<Material> set = EnumSet.noneOf(Material.class);
|
||||
for (Material m : Material.values()) {
|
||||
String n = m.name();
|
||||
if (m.isLegacy()) continue;
|
||||
if (n.equals("GLASS")
|
||||
|| n.equals("TINTED_GLASS")
|
||||
|| n.endsWith("_STAINED_GLASS")
|
||||
|| n.equals("ICE")
|
||||
|| n.equals("FROSTED_ICE")
|
||||
|| n.equals("SLIME_BLOCK")
|
||||
|| n.equals("HONEY_BLOCK")
|
||||
|| n.endsWith("_LEAVES")) set.add(m);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private final BlockModelRegistry registry;
|
||||
private final FluidRenderer fluids;
|
||||
private final BiomeTintProvider tintProvider;
|
||||
private final SkyRenderer skyRenderer;
|
||||
private final double maxDistance;
|
||||
@@ -98,6 +80,7 @@ public final class SnapshotRaytracer {
|
||||
double maxDistance,
|
||||
int reflectionDepth) {
|
||||
this.registry = registry;
|
||||
this.fluids = new FluidRenderer(registry, registry.textures());
|
||||
this.tintProvider = tintProvider;
|
||||
this.skyRenderer = skyRenderer;
|
||||
this.maxDistance = maxDistance;
|
||||
@@ -105,33 +88,57 @@ public final class SnapshotRaytracer {
|
||||
this.maxSteps = (int) (maxDistance * 3) + 3;
|
||||
}
|
||||
|
||||
/** The fluid the camera at {@code origin} is in and its fog (once per photo, see {@link CameraMedium}). */
|
||||
public CameraMedium cameraMedium(WorldSnapshot snapshot, Vector origin) {
|
||||
return CameraMedium.at(snapshot, origin, tintProvider);
|
||||
}
|
||||
|
||||
public int trace(WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene) {
|
||||
return trace(snapshot, origin, direction, sky, scene, reflectionDepth);
|
||||
return trace(snapshot, origin, direction, sky, scene, cameraMedium(snapshot, origin));
|
||||
}
|
||||
|
||||
/** As {@link #trace(WorldSnapshot, Vector, Vector, SkyContext, EntityScene)} with the camera medium given. */
|
||||
public int trace(
|
||||
WorldSnapshot snapshot,
|
||||
Vector origin,
|
||||
Vector direction,
|
||||
SkyContext sky,
|
||||
EntityScene scene,
|
||||
CameraMedium medium) {
|
||||
return trace(snapshot, origin, direction, sky, scene, reflectionDepth, medium);
|
||||
}
|
||||
|
||||
private int trace(
|
||||
WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) {
|
||||
WorldSnapshot snapshot,
|
||||
Vector origin,
|
||||
Vector direction,
|
||||
SkyContext sky,
|
||||
EntityScene scene,
|
||||
int depth,
|
||||
CameraMedium medium) {
|
||||
double ox = origin.getX(), oy = origin.getY(), oz = origin.getZ();
|
||||
double dx = direction.getX(), dy = direction.getY(), dz = direction.getZ();
|
||||
|
||||
int skyColor = skyRenderer.colorFor(direction, origin, sky);
|
||||
BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth);
|
||||
BlockCursor blocks = new BlockCursor(snapshot, origin, direction, sky, scene, depth, medium);
|
||||
boolean entities = scene != null && !scene.isEmpty();
|
||||
|
||||
// Translucent surfaces in front of the final one, front to back (shaded colour, alpha 0..1 or ADDITIVE,
|
||||
// ray t).
|
||||
// ray t, already lit: fluid faces carry their own sky light).
|
||||
int[] layerColor = null;
|
||||
double[] layerAlpha = null, layerT = null;
|
||||
boolean[] layerLit = null;
|
||||
int layers = 0;
|
||||
double transmittance = 1;
|
||||
|
||||
int finalColor = skyColor;
|
||||
double finalT = Double.POSITIVE_INFINITY; // sky
|
||||
boolean finalLit = false;
|
||||
double minT = 0;
|
||||
BlockHit pending = null; // nearest block surface not consumed yet
|
||||
|
||||
// Walk the surfaces along the ray in depth order (blocks and entities interleaved); translucent ones
|
||||
// are collected as layers and the ray continues behind them, the first opaque one ends it.
|
||||
// Walk the surfaces along the ray in depth order (blocks, fluid faces and entities interleaved);
|
||||
// translucent ones are collected as layers and the ray continues behind them, the first opaque one ends it.
|
||||
while (true) {
|
||||
if (pending == null) pending = blocks.next(minT);
|
||||
double limit = pending != null ? pending.t : maxDistance;
|
||||
@@ -139,6 +146,7 @@ public final class SnapshotRaytracer {
|
||||
int color;
|
||||
double t;
|
||||
boolean additive = false;
|
||||
boolean lit = false;
|
||||
if (eh != null) {
|
||||
// Emissive additive surfaces (energy swirl) and layers without cardinal lighting (eyes, breeze
|
||||
// wind) are unshaded: vanilla NO_CARDINAL_LIGHTING (+ EMISSIVE).
|
||||
@@ -149,6 +157,7 @@ public final class SnapshotRaytracer {
|
||||
} else if (pending != null) {
|
||||
color = pending.color;
|
||||
t = pending.t;
|
||||
lit = pending.lit;
|
||||
pending = null;
|
||||
} else {
|
||||
break;
|
||||
@@ -168,58 +177,69 @@ public final class SnapshotRaytracer {
|
||||
// Opaque, or what lies behind would contribute next to nothing: this surface ends the ray.
|
||||
finalColor = color;
|
||||
finalT = t;
|
||||
finalLit = lit;
|
||||
break;
|
||||
}
|
||||
if (layerColor == null) {
|
||||
layerColor = new int[MAX_TRANSLUCENT_LAYERS];
|
||||
layerAlpha = new double[MAX_TRANSLUCENT_LAYERS];
|
||||
layerT = new double[MAX_TRANSLUCENT_LAYERS];
|
||||
layerLit = new boolean[MAX_TRANSLUCENT_LAYERS];
|
||||
}
|
||||
layerColor[layers] = color;
|
||||
layerAlpha[layers] = additive ? ADDITIVE : a;
|
||||
layerT[layers] = t;
|
||||
layerLit[layers] = lit;
|
||||
layers++;
|
||||
if (!additive) transmittance *= 1 - a;
|
||||
minT = t + LAYER_EPS;
|
||||
}
|
||||
|
||||
// Compose back to front: final surface (fogged), the liquid volume where it lies, translucent layers.
|
||||
// Surfaces under water (seen from above it) are dimmed by their depth like vanilla sky light.
|
||||
int color = Double.isInfinite(finalT)
|
||||
? finalColor
|
||||
: fog(blocks.underwaterLight(finalColor, finalT), finalT, skyColor);
|
||||
boolean liquidPending = blocks.liquidT >= 0 && blocks.liquidT < finalT;
|
||||
// Compose back to front: final surface, translucent layers (fluid faces among them), each fogged.
|
||||
// Surfaces under water are dimmed by their depth like vanilla sky light.
|
||||
int color;
|
||||
if (Double.isInfinite(finalT)) {
|
||||
color = medium.inFluid() ? medium.sky(finalColor, dx, dy, dz) : finalColor;
|
||||
} else {
|
||||
int lit = finalLit ? finalColor : blocks.underwaterLight(finalColor, finalT);
|
||||
color = fog(lit, finalT, skyColor, medium);
|
||||
}
|
||||
for (int i = layers - 1; i >= 0; i--) {
|
||||
if (liquidPending && layerT[i] < blocks.liquidT) {
|
||||
color = blocks.applyLiquid(color, finalT, skyColor);
|
||||
liquidPending = false;
|
||||
}
|
||||
if (layerAlpha[i] == ADDITIVE) {
|
||||
// Added light fades with distance like the surface it would otherwise be.
|
||||
color = ColorUtil.add(color, ColorUtil.shade(layerColor[i], 1 - fogFactor(layerT[i])));
|
||||
double f = medium.inFluid() ? medium.fogValue(layerT[i]) : fogFactor(layerT[i]);
|
||||
color = ColorUtil.add(color, ColorUtil.shade(layerColor[i], 1 - f));
|
||||
} else {
|
||||
int layer = blocks.underwaterLight(layerColor[i], layerT[i]);
|
||||
color = ColorUtil.mix(fog(layer, layerT[i], skyColor), color, layerAlpha[i], 1 - layerAlpha[i]);
|
||||
int layer = layerLit[i] ? layerColor[i] : blocks.underwaterLight(layerColor[i], layerT[i]);
|
||||
color = ColorUtil.mix(fog(layer, layerT[i], skyColor, medium), color, layerAlpha[i], 1 - layerAlpha[i]);
|
||||
}
|
||||
}
|
||||
if (liquidPending) color = blocks.applyLiquid(color, finalT, skyColor);
|
||||
return color & 0xFFFFFF;
|
||||
}
|
||||
|
||||
/** Distance fog: the camera fluid's fog inside water/lava, the atmospheric one otherwise. */
|
||||
private int fog(int color, double distance, int skyColor, CameraMedium medium) {
|
||||
if (medium.inFluid()) return medium.fog(color, distance);
|
||||
return fog(color, distance, skyColor);
|
||||
}
|
||||
|
||||
private int fog(int color, double distance, int skyColor) {
|
||||
double f = fogFactor(distance);
|
||||
return f > 0 ? ColorUtil.mix(color, skyColor, 1 - f, f) : color;
|
||||
}
|
||||
|
||||
/** A block surface found by the {@link BlockCursor}: shaded/tinted ARGB colour and ray parameter. */
|
||||
private record BlockHit(int color, double t) {}
|
||||
/**
|
||||
* A block surface found by the {@link BlockCursor}: shaded/tinted ARGB colour, ray parameter and whether it is
|
||||
* already lit (fluid faces carry their own sky light).
|
||||
*/
|
||||
private record BlockHit(int color, double t, boolean lit) {}
|
||||
|
||||
/**
|
||||
* Walks the voxel grid along one ray and hands out block surfaces in depth order. It stays on the voxel
|
||||
* of the last returned hit, so a later call (with {@code minT} behind that hit) also finds surfaces
|
||||
* further inside the same block (slime/honey block cores, the far side of a pane). Liquids are not
|
||||
* returned as surfaces: the first one is recorded (colour, transparency, reflection) and blended as a
|
||||
* volume between its surface and the final surface, as before.
|
||||
* further inside the same block (slime/honey block cores, the far side of a pane, the water polygons of a
|
||||
* waterlogged block). Fluids are polygons like vanilla's FluidRenderer ({@link FluidRenderer}): every drawn
|
||||
* water face the ray crosses is a translucent layer (texture alpha), lava faces are opaque.
|
||||
*/
|
||||
private final class BlockCursor {
|
||||
private final WorldSnapshot snapshot;
|
||||
@@ -227,28 +247,30 @@ public final class SnapshotRaytracer {
|
||||
private final SkyContext sky;
|
||||
private final EntityScene scene;
|
||||
private final int depth;
|
||||
private final CameraMedium medium;
|
||||
private final double ox, oy, oz, dx, dy, dz;
|
||||
private final VoxelDDA dda;
|
||||
private int steps;
|
||||
private boolean done;
|
||||
|
||||
double liquidT = -1;
|
||||
// The ray starts inside a liquid (underwater photo): the liquid acts as a fog along the path instead.
|
||||
private final boolean startsInLiquid;
|
||||
private Material left; // material of the voxel the ray passed through last
|
||||
private int liquidColor;
|
||||
private double liquidFactor;
|
||||
// The ray met water (a water cell, or the camera is in water): surfaces may need the underwater light.
|
||||
private boolean sawWater;
|
||||
private boolean reflected;
|
||||
private int reflectionColor;
|
||||
private double reflectionFactor;
|
||||
|
||||
BlockCursor(
|
||||
WorldSnapshot snapshot, Vector origin, Vector direction, SkyContext sky, EntityScene scene, int depth) {
|
||||
WorldSnapshot snapshot,
|
||||
Vector origin,
|
||||
Vector direction,
|
||||
SkyContext sky,
|
||||
EntityScene scene,
|
||||
int depth,
|
||||
CameraMedium medium) {
|
||||
this.snapshot = snapshot;
|
||||
this.direction = direction;
|
||||
this.sky = sky;
|
||||
this.scene = scene;
|
||||
this.depth = depth;
|
||||
this.medium = medium;
|
||||
this.ox = origin.getX();
|
||||
this.oy = origin.getY();
|
||||
this.oz = origin.getZ();
|
||||
@@ -256,8 +278,7 @@ public final class SnapshotRaytracer {
|
||||
this.dy = direction.getY();
|
||||
this.dz = direction.getZ();
|
||||
this.dda = new VoxelDDA(ox, oy, oz, dx, dy, dz);
|
||||
BlockData start = snapshot.getBlockData((int) Math.floor(ox), (int) Math.floor(oy), (int) Math.floor(oz));
|
||||
this.startsInLiquid = start.getMaterial() != Material.AIR && registry.get(start).transparency > 0;
|
||||
this.sawWater = medium.fluid() == FluidRenderer.WATER;
|
||||
}
|
||||
|
||||
/** The next block surface with {@code t > minT}, or null when the ray leaves the render distance. */
|
||||
@@ -271,29 +292,21 @@ public final class SnapshotRaytracer {
|
||||
BlockData data = snapshot.getBlockData(bx, by, bz);
|
||||
Material material = data.getMaterial();
|
||||
if (material == Material.AIR) {
|
||||
advance(material);
|
||||
advance();
|
||||
continue;
|
||||
}
|
||||
ResolvedModel model = registry.get(data);
|
||||
FaceHit hit =
|
||||
ElementIntersector.intersect(model, ox - bx, oy - by, oz - bz, dx, dy, dz, bx, by, bz, minT);
|
||||
if (hit == null) {
|
||||
advance(material);
|
||||
continue;
|
||||
int fluidState = FluidRenderer.state(data);
|
||||
if (fluidState != 0) {
|
||||
if (FluidRenderer.type(fluidState) == FluidRenderer.WATER) sawWater = true;
|
||||
FluidRenderer.Crossing c = fluids.nearest(
|
||||
snapshot::getBlockData, bx, by, bz, data, fluidState, ox, oy, oz, dx, dy, dz, minT);
|
||||
if (c != null && (hit == null || c.t < hit.t())) return fluidHit(c, bx, by, bz);
|
||||
}
|
||||
if (model.transparency > 0) {
|
||||
if (liquidT >= 0 && material == Material.WATER && WATER_OVERLAY_NEIGHBOURS.contains(left)) {
|
||||
// Under water, leaving glass/ice/slime/honey/leaves into water again: vanilla FluidRenderer
|
||||
// draws the water's faces against such non-occluding blocks (side faces with the water
|
||||
// overlay texture), facing into the block, so from inside it the far wall is a water layer.
|
||||
int c = shadeAndTint(hit, data, snapshot, bx, by, bz);
|
||||
int a = (int) Math.round((1 - model.transparency) * 255);
|
||||
advance(material);
|
||||
return new BlockHit(
|
||||
ColorUtil.argb(a, ColorUtil.red(c), ColorUtil.green(c), ColorUtil.blue(c)), hit.t());
|
||||
}
|
||||
recordLiquid(model, hit, data, bx, by, bz);
|
||||
advance(material);
|
||||
if (hit == null) {
|
||||
advance();
|
||||
continue;
|
||||
}
|
||||
int color = shadeAndTint(hit, data, snapshot, bx, by, bz);
|
||||
@@ -301,76 +314,69 @@ public final class SnapshotRaytracer {
|
||||
minT = hit.t(); // inner face between two blocks of the same translucent kind: not drawn
|
||||
continue;
|
||||
}
|
||||
return new BlockHit(color, hit.t());
|
||||
return new BlockHit(color, hit.t(), false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Steps to the next voxel; {@code material} is the one of the voxel being left. */
|
||||
private void advance(Material material) {
|
||||
left = material;
|
||||
private void advance() {
|
||||
dda.advance();
|
||||
steps++;
|
||||
}
|
||||
|
||||
private void recordLiquid(ResolvedModel model, FaceHit hit, BlockData data, int bx, int by, int bz) {
|
||||
if (!reflected && model.reflection > 0 && depth > 0) {
|
||||
Vector reflectDir = MathUtil.reflectVector(direction, hit.normal());
|
||||
Vector reflectStart =
|
||||
hit.point().clone().add(hit.normal().clone().multiply(1e-3));
|
||||
reflectionColor = trace(snapshot, reflectStart, reflectDir, sky, scene, depth - 1);
|
||||
reflectionFactor = model.reflection;
|
||||
reflected = true;
|
||||
}
|
||||
if (liquidT < 0) {
|
||||
liquidT = hit.t();
|
||||
liquidColor = shadeAndTint(hit, data, snapshot, bx, by, bz);
|
||||
liquidFactor = model.transparency;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The liquid over {@code behind} (everything beyond its surface, already dimmed by
|
||||
* {@link #underwaterLight}): seen from above, one translucent surface layer like vanilla, independent of
|
||||
* the path length through the liquid (before, the surface weight grew with the path, so blocks a few
|
||||
* blocks deep and anything seen at a shallow angle vanished in the water colour). From inside the liquid
|
||||
* the path-length model stays (a fog). Plus reflection.
|
||||
* A crossed fluid polygon as a surface: texel × biome water colour (water) × face shade × sky light of the
|
||||
* face's cell (max with the one above, like vanilla's light coords; water only, lava glows). The first
|
||||
* water surface entered from the air also reflects (folded into the layer colour/alpha).
|
||||
*/
|
||||
int applyLiquid(int behind, double finalT, int skyColor) {
|
||||
int surface = fog(liquidColor, liquidT, skyColor);
|
||||
double surfaceWeight = 1 - liquidFactor;
|
||||
if (startsInLiquid) {
|
||||
double depthIn = Double.isInfinite(finalT) ? 0 : finalT - liquidT;
|
||||
surfaceWeight *= 1 + depthIn / 5.0;
|
||||
private BlockHit fluidHit(FluidRenderer.Crossing c, int bx, int by, int bz) {
|
||||
int color = c.texel;
|
||||
boolean water = c.type == FluidRenderer.WATER;
|
||||
double light = 1;
|
||||
if (water) {
|
||||
BiomeTint tint = blendedTint(snapshot, bx, by, bz);
|
||||
if (tint != null) color = ColorUtil.multiply(color, tint.water());
|
||||
int level = Math.max(
|
||||
FluidRenderer.skyLevel(snapshot::getBlockData, c.lightX, c.lightY, c.lightZ),
|
||||
FluidRenderer.skyLevel(snapshot::getBlockData, c.lightX, c.lightY + 1, c.lightZ));
|
||||
if (level < 15) light = SKY_LIGHT[level];
|
||||
}
|
||||
int c = ColorUtil.mix(behind, surface, liquidFactor, surfaceWeight);
|
||||
if (reflected) c = ColorUtil.mix(c, reflectionColor, 1 - reflectionFactor, reflectionFactor);
|
||||
return c;
|
||||
color = ColorUtil.shade(color, c.shade * light);
|
||||
if (water && c.entering && !reflected && depth > 0 && !medium.inFluid()) {
|
||||
reflected = true;
|
||||
double px = ox + dx * c.t, py = oy + dy * c.t, pz = oz + dz * c.t;
|
||||
Vector normal = new Vector(c.nx, c.ny, c.nz);
|
||||
Vector reflectDir = MathUtil.reflectVector(direction, normal);
|
||||
Vector reflectStart = new Vector(px, py, pz).add(normal.clone().multiply(1e-3));
|
||||
int reflection = trace(snapshot, reflectStart, reflectDir, sky, scene, depth - 1, CameraMedium.AIR);
|
||||
// mix(layer over behind, reflection): a layer with alpha a' = 1 - (1-a)(1-r).
|
||||
double a = ColorUtil.alpha(color) / 255.0, r = WATER_REFLECTION;
|
||||
double a2 = 1 - (1 - a) * (1 - r);
|
||||
double wl = a * (1 - r) / a2, wr = r / a2;
|
||||
int mixed = ColorUtil.mix(color, reflection, wl, wr);
|
||||
color = ColorUtil.argb(
|
||||
(int) Math.round(a2 * 255),
|
||||
ColorUtil.red(mixed),
|
||||
ColorUtil.green(mixed),
|
||||
ColorUtil.blue(mixed));
|
||||
}
|
||||
return new BlockHit(color, c.t, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code color} of a surface at ray parameter {@code t} dimmed like vanilla sky light under water, when it
|
||||
* lies beyond the water surface and the ray started above it: level 15 minus the blocks from the block in
|
||||
* front of the surface up to the topmost water block of that column (water, and non-occluding blocks below
|
||||
* it such as glass, cost one level each). Unchanged without water on the ray.
|
||||
* {@code color} of a surface at ray parameter {@code t} dimmed like vanilla sky light under water: by the
|
||||
* light level of the block in front of the surface (see {@link FluidRenderer#skyLevel}). Unchanged when the
|
||||
* ray never met water.
|
||||
*/
|
||||
int underwaterLight(int color, double t) {
|
||||
if (liquidT < 0 || startsInLiquid || !(t > liquidT) || Double.isInfinite(t)) return color;
|
||||
if (!sawWater || Double.isInfinite(t)) return color;
|
||||
// A point just in front of the surface (back along the ray), i.e. in the block that lights it.
|
||||
double back = 1e-4 / Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
int bx = (int) Math.floor(ox + dx * (t - back));
|
||||
int by = (int) Math.floor(oy + dy * (t - back));
|
||||
int bz = (int) Math.floor(oz + dz * (t - back));
|
||||
int water = 0;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
Material m = snapshot.getBlockData(bx, by + i, bz).getMaterial();
|
||||
if (m == Material.WATER) {
|
||||
water = i + 1;
|
||||
} else if (m == Material.AIR || m == Material.CAVE_AIR || m == Material.VOID_AIR || m.isOccluding()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return water == 0 ? color : ColorUtil.shade(color, SKY_LIGHT[15 - water]);
|
||||
int level = FluidRenderer.skyLevel(snapshot::getBlockData, bx, by, bz);
|
||||
return level >= 15 ? color : ColorUtil.shade(color, SKY_LIGHT[level]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,8 @@ import eu.mhsl.minecraft.pixelpics.render.entity.ObjectState;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.BlockEntityBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.CemBaker;
|
||||
import eu.mhsl.minecraft.pixelpics.render.entity.cem.ItemGeometry;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.CameraMedium;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.FluidRenderer;
|
||||
import eu.mhsl.minecraft.pixelpics.render.raytrace.SnapshotRaytracer;
|
||||
import eu.mhsl.minecraft.pixelpics.render.sky.SkyContext;
|
||||
import eu.mhsl.minecraft.pixelpics.render.sky.SkyRenderer;
|
||||
@@ -50,6 +52,8 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
* horizontal FOV follows from the image aspect ratio. Square photos: 70° both ways.
|
||||
*/
|
||||
public static final double V_FOV_DEG = 70;
|
||||
// Camera.getFov in water/lava: fov · lerp(fovEffectScale = 1, 1, 6/7).
|
||||
private static final double FLUID_FOV_SCALE = 6.0 / 7.0;
|
||||
|
||||
private static final double MAX_DISTANCE = 256;
|
||||
private static final int REFLECTION_DEPTH = 4;
|
||||
@@ -115,7 +119,14 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
public RenderJob prepare(Location eyeLocation, Resolution resolution, UUID shooter) {
|
||||
int superW = resolution.getWidth() * SSAA;
|
||||
int superH = resolution.getHeight() * SSAA;
|
||||
List<Vector> rayMap = buildRayMap(eyeLocation, superW, superH);
|
||||
// Vanilla Camera.getFov: in water or lava the FOV shrinks to 6/7 (FOV effects slider at its default 100%).
|
||||
World eyeWorld = eyeLocation.getWorld();
|
||||
boolean inFluid = eyeWorld != null
|
||||
&& FluidRenderer.cameraFluid(
|
||||
eyeWorld::getBlockData, eyeLocation.getX(), eyeLocation.getY(), eyeLocation.getZ())
|
||||
!= FluidRenderer.NONE;
|
||||
List<Vector> rayMap =
|
||||
buildRayMap(eyeLocation, superW, superH, inFluid ? V_FOV_DEG * FLUID_FOV_SCALE : V_FOV_DEG);
|
||||
WorldSnapshot snapshot = SnapshotBuilder.build(eyeLocation, rayMap, MAX_DISTANCE, logger);
|
||||
Collection<Entity> nearby = FrustumBounds.nearbyEntities(eyeLocation, rayMap, MAX_DISTANCE);
|
||||
List<EntityState> entities = EntitySnapshotBuilder.build(nearby, shooter);
|
||||
@@ -170,9 +181,10 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
job.objects(),
|
||||
objectBaker);
|
||||
|
||||
CameraMedium medium = raytracer.cameraMedium(snapshot, origin);
|
||||
int[] superBuf = new int[rayMap.size()];
|
||||
runParallel(() -> IntStream.range(0, rayMap.size()).parallel().forEach(i -> {
|
||||
if (!cancelled.get()) superBuf[i] = raytracer.trace(snapshot, origin, rayMap.get(i), sky, scene);
|
||||
if (!cancelled.get()) superBuf[i] = raytracer.trace(snapshot, origin, rayMap.get(i), sky, scene, medium);
|
||||
}));
|
||||
|
||||
BufferedImage image = new BufferedImage(finalW, finalH, BufferedImage.TYPE_INT_RGB);
|
||||
@@ -216,13 +228,18 @@ public class DefaultScreenRenderer implements Renderer {
|
||||
* from the yaw/pitch of {@code eye} (no roll), so looking straight up or down keeps the yaw orientation.
|
||||
*/
|
||||
public static List<Vector> buildRayMap(Location eye, int width, int height) {
|
||||
return buildRayMap(eye, width, height, V_FOV_DEG);
|
||||
}
|
||||
|
||||
/** {@link #buildRayMap(Location, int, int)} with the vertical FOV {@code vFovDeg}. */
|
||||
public static List<Vector> buildRayMap(Location eye, int width, int height, double vFovDeg) {
|
||||
double yaw = Math.toRadians(eye.getYaw());
|
||||
Vector forward = eye.getDirection();
|
||||
// Player's right in Minecraft's coordinates (yaw 0 = south/+Z → right = west/−X).
|
||||
Vector right = new Vector(-Math.cos(yaw), 0, -Math.sin(yaw));
|
||||
Vector up = right.getCrossProduct(forward).normalize();
|
||||
|
||||
double tanY = Math.tan(Math.toRadians(V_FOV_DEG) / 2);
|
||||
double tanY = Math.tan(Math.toRadians(vFovDeg) / 2);
|
||||
double tanX = tanY * width / height;
|
||||
|
||||
List<Vector> rayMap = new ArrayList<>(width * height);
|
||||
|
||||
@@ -31,6 +31,9 @@ public final class BiomeClimate {
|
||||
* @param foliage explicit foliage color override, or {@link #NONE}
|
||||
* @param dryFoliage explicit dry-foliage color override, or {@link #NONE}
|
||||
* @param grassModifier vanilla {@code grass_color_modifier} ("swamp", "dark_forest") or null
|
||||
* @param waterFogColor RGB underwater fog colour (attribute {@code minecraft:visual/water_fog_color})
|
||||
* @param waterFogStart underwater fog start distance ({@code minecraft:visual/water_fog_start_distance})
|
||||
* @param waterFogEnd underwater fog end distance ({@code minecraft:visual/water_fog_end_distance})
|
||||
*/
|
||||
public record Climate(
|
||||
double temperature,
|
||||
@@ -39,9 +42,52 @@ public final class BiomeClimate {
|
||||
int grass,
|
||||
int foliage,
|
||||
int dryFoliage,
|
||||
String grassModifier) {}
|
||||
String grassModifier,
|
||||
int waterFogColor,
|
||||
double waterFogStart,
|
||||
double waterFogEnd) {
|
||||
|
||||
Climate(
|
||||
double temperature,
|
||||
double downfall,
|
||||
int water,
|
||||
int grass,
|
||||
int foliage,
|
||||
int dryFoliage,
|
||||
String grassModifier) {
|
||||
this(
|
||||
temperature,
|
||||
downfall,
|
||||
water,
|
||||
grass,
|
||||
foliage,
|
||||
dryFoliage,
|
||||
grassModifier,
|
||||
DEFAULT_WATER_FOG_COLOR,
|
||||
DEFAULT_WATER_FOG_START,
|
||||
DEFAULT_WATER_FOG_END);
|
||||
}
|
||||
|
||||
Climate withWaterFog(int color, double endFactor) {
|
||||
return new Climate(
|
||||
temperature,
|
||||
downfall,
|
||||
water,
|
||||
grass,
|
||||
foliage,
|
||||
dryFoliage,
|
||||
grassModifier,
|
||||
color,
|
||||
waterFogStart,
|
||||
waterFogEnd * endFactor);
|
||||
}
|
||||
}
|
||||
|
||||
public static final int DEFAULT_WATER = 0x3F76E4;
|
||||
// Vanilla 26.3 EnvironmentAttributes defaults (no dimension type sets them).
|
||||
public static final int DEFAULT_WATER_FOG_COLOR = 0x050533;
|
||||
public static final double DEFAULT_WATER_FOG_START = -8;
|
||||
public static final double DEFAULT_WATER_FOG_END = 96;
|
||||
public static final Climate DEFAULT = new Climate(0.8, 0.4, DEFAULT_WATER, NONE, NONE, NONE, null);
|
||||
|
||||
private static final String DATA_PATH = "data/minecraft/worldgen/biome/";
|
||||
@@ -123,6 +169,20 @@ public final class BiomeClimate {
|
||||
put("windswept_hills", 0.2, 0.3, 0x3F76E4, NONE, NONE, NONE, null);
|
||||
put("windswept_savanna", 2.0, 0.0, 0x3F76E4, NONE, NONE, NONE, null);
|
||||
put("wooded_badlands", 2.0, 0.0, 0x3F76E4, 0x90814D, 0x9E814D, NONE, null);
|
||||
// Biome attributes minecraft:visual/water_fog_color (+ water_fog_end_distance multiply modifier).
|
||||
fog("cherry_grove", 0x5DB7EF, 1);
|
||||
fog("dappled_forest", 0x375154, 1);
|
||||
fog("deep_lukewarm_ocean", 0x041633, 1);
|
||||
fog("lukewarm_ocean", 0x041633, 1);
|
||||
fog("mangrove_swamp", 0x4D7A60, 0.85);
|
||||
fog("pale_garden", 0x556980, 1);
|
||||
fog("sulfur_caves", 0x17543C, 1);
|
||||
fog("swamp", 0x232317, 0.85);
|
||||
fog("warm_ocean", 0x041F33, 1);
|
||||
}
|
||||
|
||||
private static void fog(String key, int color, double endFactor) {
|
||||
FALLBACK.put(key, FALLBACK.get(key).withWaterFog(color, endFactor));
|
||||
}
|
||||
|
||||
private BiomeClimate() {}
|
||||
@@ -142,6 +202,7 @@ public final class BiomeClimate {
|
||||
.getAsJsonObject();
|
||||
JsonObject effects = root.has("effects") ? root.getAsJsonObject("effects") : new JsonObject();
|
||||
JsonElement modifier = effects.get("grass_color_modifier");
|
||||
JsonObject attributes = root.has("attributes") ? root.getAsJsonObject("attributes") : new JsonObject();
|
||||
return Optional.of(new Climate(
|
||||
root.has("temperature") ? root.get("temperature").getAsDouble() : DEFAULT.temperature(),
|
||||
root.has("downfall") ? root.get("downfall").getAsDouble() : DEFAULT.downfall(),
|
||||
@@ -149,12 +210,42 @@ public final class BiomeClimate {
|
||||
color(effects, "grass_color", NONE),
|
||||
color(effects, "foliage_color", NONE),
|
||||
color(effects, "dry_foliage_color", NONE),
|
||||
modifier != null && modifier.isJsonPrimitive() ? modifier.getAsString() : null));
|
||||
modifier != null && modifier.isJsonPrimitive() ? modifier.getAsString() : null,
|
||||
color(attributes, "minecraft:visual/water_fog_color", DEFAULT_WATER_FOG_COLOR),
|
||||
floatAttribute(attributes, "minecraft:visual/water_fog_start_distance", DEFAULT_WATER_FOG_START),
|
||||
floatAttribute(attributes, "minecraft:visual/water_fog_end_distance", DEFAULT_WATER_FOG_END)));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A float environment attribute of a biome applied to the default: a plain value overrides it, an object
|
||||
* {@code {"argument": a, "modifier": op}} applies vanilla's float modifier (override/add/subtract/multiply/
|
||||
* minimum/maximum).
|
||||
*/
|
||||
private static double floatAttribute(JsonObject attributes, String key, double base) {
|
||||
JsonElement el = attributes.get(key);
|
||||
if (el == null) return base;
|
||||
try {
|
||||
if (el.isJsonPrimitive()) return el.getAsDouble();
|
||||
if (!el.isJsonObject()) return base;
|
||||
JsonObject o = el.getAsJsonObject();
|
||||
double a = o.get("argument").getAsDouble();
|
||||
String op = o.has("modifier") ? o.get("modifier").getAsString() : "override";
|
||||
return switch (op) {
|
||||
case "add" -> base + a;
|
||||
case "subtract" -> base - a;
|
||||
case "multiply" -> base * a;
|
||||
case "minimum" -> Math.min(base, a);
|
||||
case "maximum" -> Math.max(base, a);
|
||||
default -> a;
|
||||
};
|
||||
} catch (RuntimeException e) {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
/** Colors are either "#rrggbb" strings or packed integers, depending on the data version. */
|
||||
private static int color(JsonObject effects, String key, int fallback) {
|
||||
JsonElement el = effects.get(key);
|
||||
|
||||
@@ -29,6 +29,11 @@ public final class BiomeTintProvider {
|
||||
return cache.computeIfAbsent(keyOf(biome), this::compute);
|
||||
}
|
||||
|
||||
/** The biome's climate/effects/attribute data (water fog etc.), see {@link BiomeClimate}. */
|
||||
public BiomeClimate.Climate climate(Biome biome) {
|
||||
return BiomeClimate.forKey(keyOf(biome));
|
||||
}
|
||||
|
||||
private String keyOf(Biome biome) {
|
||||
try {
|
||||
return biome.getKey().getKey();
|
||||
|
||||
@@ -12,6 +12,7 @@ import javax.imageio.ImageIO;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -212,6 +213,30 @@ public class PPTest extends JavaPlugin {
|
||||
scenes.put("items_inverted", new Scene("items_inverted", this::itemsInverted, 3, 2.4, 7, 3, 0.6, 1.5));
|
||||
scenes.put("xmas_chests", new Scene("xmas_chests", this::xmasChests, 6, 3.6, 9.5, 6, 0.5, 1.5));
|
||||
scenes.put("frames_markers", new Scene("frames_markers", this::framesMarkers, 2.5, 2.4, 4.2, 2.5, 2.4, 0));
|
||||
// Fluids (vanilla FluidRenderer / FogRenderer): liq_flow = water flowing with physics (rendered 200 ticks after
|
||||
// building): a source on a 3-high terrace spreading and falling over its edges, a lone source spreading on
|
||||
// the floor, a sunk 3x3 source pool (surface 8/9 below the rim). liq_logged = waterlogged blocks in a 1-deep
|
||||
// pool, standing in the air (water inside, surface at 8/9) and under water. liq_under* = camera in a
|
||||
// 22x22x8 tank with concrete pillars at growing distances (water fog), seen from 3 and 7 deep, looking up at
|
||||
// the surface (bridge + lily pads above), just under the surface, from above; _swamp/_warm = same tank in a
|
||||
// swamp / warm ocean (water_fog_color, swamp end x0.85, water_color). liq_lava = lava pool, flowing levels
|
||||
// and a falling column; liq_lava_in = camera inside the lava (lava fog).
|
||||
scenes.put("liq_flow", new Scene("liq_flow", this::liqFlow, -3, 8, -9, 6, 0, 0));
|
||||
scenes.put("liq_flow_close", new Scene("liq_flow_close", this::liqFlow, 7.5, 1.6, 9, 11, 0, 5));
|
||||
scenes.put("liq_flow_fall", new Scene("liq_flow_fall", this::liqFlow, -4, 3, 7, 2, 1.5, 0));
|
||||
scenes.put("liq_logged", new Scene("liq_logged", this::liqLogged, 6, 3.6, -4.5, 6, -0.5, 3.5));
|
||||
scenes.put("liq_logged_close", new Scene("liq_logged_close", this::liqLogged, 2.2, 0.7, -1.4, 3, -0.4, 1.5));
|
||||
scenes.put("liq_logged_air", new Scene("liq_logged_air", this::liqLogged, 4, 1.6, 2.6, 4.5, 0.4, 5.5));
|
||||
scenes.put("liq_under", new Scene("liq_under", o -> liqUnder(o, Biome.PLAINS), -3.5, -3, -8.5, 12, -5, 6));
|
||||
scenes.put("liq_under_deep", new Scene("liq_under_deep", o -> liqUnder(o, Biome.PLAINS), -3.5, -7, -8.5, 12, -6, 6));
|
||||
scenes.put("liq_under_up", new Scene("liq_under_up", o -> liqUnder(o, Biome.PLAINS), 6.2, -6, -4, 6.4, 0, 0.5));
|
||||
scenes.put("liq_under_shallow", new Scene("liq_under_shallow", o -> liqUnder(o, Biome.PLAINS), -3.5, -0.45, -8.5, 12, 0.6, 6));
|
||||
scenes.put("liq_under_top", new Scene("liq_under_top", o -> liqUnder(o, Biome.PLAINS), -3, 4, -10, 6, -6, 2));
|
||||
scenes.put("liq_under_swamp", new Scene("liq_under_swamp", o -> liqUnder(o, Biome.SWAMP), -3.5, -3, -8.5, 12, -5, 6));
|
||||
scenes.put("liq_under_warm", new Scene("liq_under_warm", o -> liqUnder(o, Biome.WARM_OCEAN), -3.5, -3, -8.5, 12, -5, 6));
|
||||
scenes.put("liq_under_warm_top", new Scene("liq_under_warm_top", o -> liqUnder(o, Biome.WARM_OCEAN), -3, 4, -10, 6, -6, 2));
|
||||
scenes.put("liq_lava", new Scene("liq_lava", this::liqLava, 4, 4.5, -6, 9, 0, 1));
|
||||
scenes.put("liq_lava_in", new Scene("liq_lava_in", this::liqLava, 3.5, -0.5, 1.5, 9, -0.5, 1.5));
|
||||
}
|
||||
|
||||
private static ItemStack shield(DyeColor base, Object... patterns) {
|
||||
@@ -838,7 +863,7 @@ public class PPTest extends JavaPlugin {
|
||||
t.printStackTrace();
|
||||
}
|
||||
// Let one tick pass (passengers/leashes settle), then render.
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : sc.name.startsWith("be_contents") ? 100 : 10);
|
||||
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : sc.name.startsWith("be_contents") ? 100 : sc.name.startsWith("liq_flow") ? 200 : 10);
|
||||
}
|
||||
|
||||
private void render(Scene sc, Location o) {
|
||||
@@ -846,7 +871,7 @@ public class PPTest extends JavaPlugin {
|
||||
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
|
||||
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
|
||||
eye.setDirection(look);
|
||||
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") || sc.name.startsWith("geo_") || sc.name.startsWith("items_") || sc.name.startsWith("xmas_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
|
||||
RenderJob job = r.prepare(eye, sc.name.endsWith("_sq") ? new Resolution(384, 384) : sc.name.startsWith("proj_") || sc.name.startsWith("rf_") || sc.name.startsWith("pose_") || sc.name.startsWith("frames") || sc.name.startsWith("geo_") || sc.name.startsWith("liq_") || sc.name.startsWith("items_") || sc.name.startsWith("xmas_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
|
||||
if (sc.name.equals("dbg")) {
|
||||
try {
|
||||
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
|
||||
@@ -2067,4 +2092,111 @@ public class PPTest extends JavaPlugin {
|
||||
}
|
||||
return mapItem(v);
|
||||
}
|
||||
|
||||
// --- fluids ------------------------------------------------------------------------------------------------
|
||||
|
||||
/** Sets the biome of the scene area (deterministic water colour / water fog). */
|
||||
private void biome(Location o, Biome biome) {
|
||||
for (int x = -8; x < 20; x += 2)
|
||||
for (int z = -14; z < 14; z += 2)
|
||||
for (int y = -16; y < 16; y += 2)
|
||||
w.setBiome(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z, biome);
|
||||
}
|
||||
|
||||
/** A block placed WITH physics (fluids schedule their flow ticks). */
|
||||
private void putPhys(Location o, int x, int y, int z, String data) {
|
||||
o.clone().add(x, y, z).getBlock().setBlockData(Bukkit.createBlockData("minecraft:" + data), true);
|
||||
}
|
||||
|
||||
private void liqFlow(Location o) {
|
||||
biome(o, Biome.PLAINS);
|
||||
// Stone rim keeps the water on the platform.
|
||||
for (int x = -5; x <= 16; x++)
|
||||
for (int z = -11; z <= 10; z++)
|
||||
if (x == -5 || x == 16 || z == -11 || z == 10) put(o, x, 0, z, "stone");
|
||||
for (int x = 0; x <= 4; x++)
|
||||
for (int z = -2; z <= 2; z++)
|
||||
for (int y = 0; y <= 2; y++) put(o, x, y, z, "stone");
|
||||
for (int x = 10; x <= 12; x++)
|
||||
for (int z = -6; z <= -4; z++) {
|
||||
put(o, x, -2, z, "sand");
|
||||
put(o, x, -1, z, "water");
|
||||
}
|
||||
putPhys(o, 2, 3, 0, "water");
|
||||
putPhys(o, 11, 0, 5, "water");
|
||||
}
|
||||
|
||||
private static final String[] LOGGED = {
|
||||
"oak_stairs[facing=north,waterlogged=true]", "stone_slab[type=bottom,waterlogged=true]",
|
||||
"stone_slab[type=top,waterlogged=true]", "oak_fence[waterlogged=true]", "glass_pane[waterlogged=true]",
|
||||
"seagrass", "kelp_plant", "tube_coral[waterlogged=true]", "oak_leaves[waterlogged=true,persistent=true]",
|
||||
"lantern[waterlogged=true]", "sea_pickle[pickles=3,waterlogged=true]", "oak_trapdoor[half=bottom,open=false,waterlogged=true]"};
|
||||
|
||||
private void liqLogged(Location o) {
|
||||
biome(o, Biome.PLAINS);
|
||||
for (int x = -1; x <= LOGGED.length; x++)
|
||||
for (int z = 0; z <= 2; z++) {
|
||||
put(o, x, -2, z, "sand");
|
||||
put(o, x, -1, z, "water");
|
||||
}
|
||||
for (int i = 0; i < LOGGED.length; i++) {
|
||||
put(o, i, -1, 1, LOGGED[i]); // in the pool
|
||||
put(o, i, 0, 5, LOGGED[i]); // in the air: water inside, surface at 8/9
|
||||
}
|
||||
// Under water: a waterlogged stair and slab with a water source above them.
|
||||
put(o, 2, 0, 8, "oak_stairs[facing=south,waterlogged=true]");
|
||||
put(o, 2, 1, 8, "water");
|
||||
put(o, 4, 0, 8, "stone_slab[type=bottom,waterlogged=true]");
|
||||
put(o, 4, 1, 8, "water");
|
||||
// Water next to glass and leaves (overlay faces) on the floor.
|
||||
put(o, 7, 0, 8, "water");
|
||||
put(o, 8, 0, 8, "glass");
|
||||
put(o, 9, 0, 8, "water");
|
||||
put(o, 10, 0, 8, "oak_leaves[persistent=true]");
|
||||
}
|
||||
|
||||
private void liqUnder(Location o, Biome biome) {
|
||||
biome(o, biome);
|
||||
for (int x = -5; x <= 17; x++)
|
||||
for (int z = -11; z <= 11; z++)
|
||||
for (int y = -9; y <= -1; y++) {
|
||||
boolean wall = x == -5 || x == 17 || z == -11 || z == 11;
|
||||
put(o, x, y, z, wall ? "stone_bricks" : y == -9 ? "sand" : "water");
|
||||
}
|
||||
String[] pillars = {"red_concrete", "yellow_concrete", "lime_concrete", "blue_concrete", "white_concrete", "orange_concrete"};
|
||||
int[][] at = {{0, 0}, {4, -4}, {8, 4}, {12, -6}, {15, 8}, {16, -10}};
|
||||
for (int i = 0; i < pillars.length; i++)
|
||||
for (int y = -8; y <= -4; y++) put(o, at[i][0], y, at[i][1], pillars[i]);
|
||||
put(o, 2, -8, 3, "gold_block");
|
||||
put(o, 2, -7, 3, "glass");
|
||||
put(o, 5, -8, 1, "oak_stairs[facing=west,waterlogged=true]");
|
||||
for (int i = 0; i < 8; i++) put(o, -2 + i * 2, -8, -6 + (i % 3), "seagrass");
|
||||
// Above the water: a bridge and lily pads, seen from below through the surface.
|
||||
for (int x = 3; x <= 9; x++) put(o, x, 2, 1, "oak_planks");
|
||||
put(o, 6, 0, -2, "lily_pad");
|
||||
put(o, 7, 0, -1, "lily_pad");
|
||||
}
|
||||
|
||||
private void liqLava(Location o) {
|
||||
biome(o, Biome.PLAINS);
|
||||
for (int x = -2; x <= 17; x++)
|
||||
for (int z = -3; z <= 5; z++)
|
||||
if (x == -2 || x == 17 || z == -3 || z == 5) put(o, x, 0, z, "stone");
|
||||
for (int x = 2; x <= 4; x++)
|
||||
for (int z = 0; z <= 2; z++) {
|
||||
put(o, x, -2, z, "basalt");
|
||||
put(o, x, -1, z, "lava");
|
||||
}
|
||||
// A source with flowing lava around it (overworld drop-off 2: amounts 8/6/4/2 = levels 0/2/4/6).
|
||||
for (int dx = -3; dx <= 3; dx++)
|
||||
for (int dz = -3; dz <= 3; dz++) {
|
||||
int d = Math.abs(dx) + Math.abs(dz);
|
||||
if (d <= 3) put(o, 9 + dx, 0, 1 + dz, d == 0 ? "lava" : "lava[level=" + (2 * d) + "]");
|
||||
}
|
||||
// Lava falling from a pillar.
|
||||
for (int y = 0; y <= 2; y++) put(o, 14, y, 1, "stone");
|
||||
put(o, 14, 3, 1, "lava");
|
||||
put(o, 15, 3, 1, "lava[level=2]");
|
||||
for (int y = 0; y <= 2; y++) put(o, 15, y, 1, "lava[level=10]");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user