/* * This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT). * * Copyright (c) 2015 contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package cubicchunks.lighting; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenCustomHashMap; import it.unimi.dsi.fastutil.ints.IntHash; import net.minecraft.util.EnumFacing; import net.minecraft.util.math.BlockPos; import net.minecraft.world.EnumSkyBlock; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import cubicchunks.util.FastCubeBlockAccess; import cubicchunks.world.ICubeProvider; import cubicchunks.world.ICubicWorld; import cubicchunks.world.IHeightMap; import cubicchunks.world.column.Column; import cubicchunks.world.cube.Cube; import static cubicchunks.util.Coords.blockToCube; import static cubicchunks.util.Coords.blockToLocal; import static cubicchunks.util.Coords.cubeToMaxBlock; import static cubicchunks.util.Coords.cubeToMinBlock; import static cubicchunks.util.Coords.getCubeCenter; import static net.minecraft.util.math.BlockPos.MutableBlockPos; /** * Notes on world.checkLightFor(): Decreasing light value: Light is recalculated starting from 0 ONLY for blocks where * rawLightValue is equal to savedLightValue (ie. updating skylight source that is not there anymore). Otherwise * existing light values are assumed to be correct. Generates and updates cube initial lighting, and propagates light * changes caused by generating cube downwards. * <p> * Used only when changes are caused by pre-populator terrain generation. * <p> * THIS SHOULD ONLY EVER BE USED ONCE PER CUBE. */ //TODO: make it also update blocklight public class FirstLightProcessor { private static final int LIGHT_UPDATE_RADIUS = 17; private static final int CUBE_RADIUS = Cube.SIZE/2; private static final int UPDATE_BUFFER_RADIUS = 1; private static final int UPDATE_RADIUS = LIGHT_UPDATE_RADIUS + CUBE_RADIUS + UPDATE_BUFFER_RADIUS; private static final IntHash.Strategy CUBE_Y_HASH = new IntHash.Strategy() { @Override public int hashCode(int e) { return e; } @Override public boolean equals(int a, int b) { return a == b; } }; private final MutableBlockPos mutablePos = new MutableBlockPos(); private final ICubeProvider cache; /** * Creates a new FirstLightProcessor for the given world. * * @param world the world for which the FirstLightProcessor will be used */ public FirstLightProcessor(ICubicWorld world) { this.cache = world.getCubeCache(); } /** * Initializes skylight in the given cube. The skylight will be consistent with respect to the world configuration * and already existing cubes. It is however possible for cubes being considered lit at this stage to be occluded * by cubes being generated further up. * * @param cube the cube whose skylight is to be initialized */ public void initializeSkylight(Cube cube) { if (cube.getCubicWorld().getProvider().getHasNoSky()) { return; } IHeightMap opacityIndex = cube.getColumn().getOpacityIndex(); int cubeMinY = cubeToMinBlock(cube.getY()); for (int localX = 0; localX < Cube.SIZE; ++localX) { for (int localZ = 0; localZ < Cube.SIZE; ++localZ) { for (int localY = Cube.SIZE - 1; localY >= 0; --localY) { if (opacityIndex.isOccluded(localX, cubeMinY + localY, localZ)) { break; } cube.setSkylight(localX, localY, localZ, 15); } } } } /** * Diffuses skylight in the given cube and all cubes affected by this update. * * @param cube the cube whose skylight is to be initialized */ public void diffuseSkylight(Cube cube) { if (cube.getCubicWorld().getProvider().getHasNoSky()) { cube.setInitialLightingDone(true); return; } ICubicWorld world = cube.getCubicWorld(); // Cache min/max Y, generating them may be expensive int[][] minBlockYArr = new int[16][16]; int[][] maxBlockYArr = new int[16][16]; int minBlockX = cubeToMinBlock(cube.getX()); int maxBlockX = cubeToMaxBlock(cube.getX()); int minBlockZ = cubeToMinBlock(cube.getZ()); int maxBlockZ = cubeToMaxBlock(cube.getZ()); // Determine the block columns that require updating. If there is nothing to update, store contradicting data so // we can skip the column later. for (int localX = 0; localX <= Cube.SIZE - 1; ++localX) { for (int localZ = 0; localZ <= Cube.SIZE - 1; ++localZ) { Pair<Integer, Integer> minMax = getMinMaxLightUpdateY(cube, localX, localZ); minBlockYArr[localX][localZ] = minMax == null ? Integer.MAX_VALUE : minMax.getLeft(); maxBlockYArr[localX][localZ] = minMax == null ? Integer.MIN_VALUE : minMax.getRight(); } } Int2ObjectMap<FastCubeBlockAccess> blockAccessMap = new Int2ObjectOpenCustomHashMap<>(10, 0.75f, CUBE_Y_HASH); Column column = cube.getColumn(); for (int blockX = minBlockX; blockX <= maxBlockX; blockX++) { for (int blockZ = minBlockZ; blockZ <= maxBlockZ; blockZ++) { this.mutablePos.setPos(blockX, this.mutablePos.getY(), blockZ); int minBlockY = minBlockYArr[blockX - minBlockX][blockZ - minBlockZ]; int maxBlockY = maxBlockYArr[blockX - minBlockX][blockZ - minBlockZ]; // If no update is needed, skip the block column. if (minBlockY > maxBlockY) { continue; } int topBlockY = getOcclusionHeight(column, blockToLocal(blockX), blockToLocal(blockZ)); // Iterate over all affected cubes. Iterable<Cube> cubes = column.getLoadedCubes(blockToCube(maxBlockY), blockToCube(minBlockY)); for (Cube otherCube : cubes) { int cubeY = otherCube.getY(); if (otherCube != cube && canStopUpdating(cube, this.mutablePos, topBlockY)) { break; } if (otherCube != cube && !cube.isInitialLightingDone()) { continue; } // Skip this cube if an update is not possible. if (!canUpdateCube(otherCube)) { int minScheduledY = Math.max(cubeToMinBlock(cubeY), minBlockY); int maxScheduledY = Math.min(cubeToMaxBlock(cubeY), maxBlockY); // Queue the update to be processed once the cube is ready for it. world.getLightingManager().queueDiffuseUpdate(otherCube, this.mutablePos.getX(), this.mutablePos.getZ(), minScheduledY, maxScheduledY); continue; } // Update the block column in this cube. if (!diffuseSkylightInBlockColumn(otherCube, this.mutablePos, minBlockY, maxBlockY, blockAccessMap)) { throw new IllegalStateException("Check light failed at " + this.mutablePos + "!"); } } } } cube.setInitialLightingDone(true); } /** * Diffuses skylight inside of the given cube in the block column specified by the given MutableBlockPos. The * update is limited vertically by minBlockY and maxBlockY. * * @param cube the cube inside of which the skylight is to be diffused * @param pos the xz-position of the block column to be updated * @param minBlockY the lower bound of the section to be updated * @param maxBlockY the upper bound of the section to be updated * * @return true if the update was successful, false otherwise */ private boolean diffuseSkylightInBlockColumn(Cube cube, MutableBlockPos pos, int minBlockY, int maxBlockY, Int2ObjectMap<FastCubeBlockAccess> blockAccessMap) { ICubicWorld world = cube.getCubicWorld(); int cubeMinBlockY = cubeToMinBlock(cube.getY()); int cubeMaxBlockY = cubeToMaxBlock(cube.getY()); int maxBlockYInCube = Math.min(cubeMaxBlockY, maxBlockY); int minBlockYInCube = Math.max(cubeMinBlockY, minBlockY); FastCubeBlockAccess blockAccess = blockAccessMap.get(cube.getY()); if (blockAccess == null) { blockAccess = new FastCubeBlockAccess(this.cache, cube, UPDATE_BUFFER_RADIUS); blockAccessMap.put(cube.getY(), blockAccess); } for (int blockY = maxBlockYInCube; blockY >= minBlockYInCube; --blockY) { pos.setY(blockY); if (!needsSkylightUpdate(blockAccess, pos)) { continue; } if (!world.checkLightFor(EnumSkyBlock.SKY, pos)) { return false; } } return true; } /** * Determines if the block at the given position requires a skylight update. * * @param access a FastCubeBlockAccess providing access to the block * @param pos the block's global position * * @return true if the specified block needs a skylight update, false otherwise */ private static boolean needsSkylightUpdate(FastCubeBlockAccess access, MutableBlockPos pos) { // Opaque blocks don't need update. Nothing can emit skylight, and skylight can't get into them nor out of them. if (access.getBlockLightOpacity(pos) >= 15) { return false; } // This is the logic that world.checkLightFor uses to determine if it should continue updating. // This is done here to avoid isAreaLoaded call (a lot of them quickly add up to a lot of time). // It first calculates the expected skylight value of this block and then it checks the neighbors' saved values, // if the saved value matches the expected value, it will be updated. int computedLight = access.computeLightValue(pos); for (EnumFacing facing : EnumFacing.values()) { pos.move(facing); int currentLight = access.getLightFor(EnumSkyBlock.SKY, pos); int currentOpacity = Math.max(1, access.getBlockLightOpacity(pos)); pos.move(facing.getOpposite()); if (computedLight == currentLight - currentOpacity) { return true; } } return false; } /** * Determines if light in the given cube can be updated. * * @param cube the cube whose light is supposed to be updated * * @return true if light in the given cube can be updated, false otherwise */ private static boolean canUpdateCube(Cube cube) { BlockPos cubeCenter = getCubeCenter(cube); return cube.getCubicWorld().testForCubes(cubeCenter, UPDATE_RADIUS, c -> c != null); } /** * Determines if the block column of the given cube as specified by the given BlockPos has valid lighting and thus * does not require further updating. * * @param cube the cube whose light is supposed to be updated * @param pos the xz-position of the block column being updated * @param topBlockY the y-coordinate of the highest block in the block column * * @return true if updating the skylight of the specified block column is no longer required, false otherwise */ private static boolean canStopUpdating(Cube cube, MutableBlockPos pos, int topBlockY) { // Note: This logic does not apply to the main cube being updated, but only to those below it! pos.setY(cube.getCoords().getMaxBlockY()); boolean isDirectSkylight = pos.getY() > topBlockY; int lightValue = cube.getLightFor(EnumSkyBlock.SKY, pos); // If the cube does not receive direct skylight and the light value does not need updating, then all blocks // further down do not need to be updated either. return !isDirectSkylight && lightValue < 15; } /** * Returns the y-coordinate of the highest occluding block in the specified block column. If there exists no such * block {@link #DEFAULT_OCCLUSION_HEIGHT} will be returned instead. * * @param column the column containing the block column * @param localX the block column's local x-coordinate * @param localZ the block column's local z-coordinate * * @return the y-coordinate of the highest occluding block in the specified block column or {@link * #DEFAULT_OCCLUSION_HEIGHT} if no such block exists */ private static int getOcclusionHeight(Column column, int localX, int localZ) { return column.getOpacityIndex().getTopBlockY(localX, localZ); } /** * Returns the y-coordinate of the highest occluding block in the specified block column, that is underneath the * cube at the given y-coordinate. If there exists no such block {@link #DEFAULT_OCCLUSION_HEIGHT} will be returned * instead. * * @param column the column containing the block column * @param blockX the block column's global x-coordinate * @param blockZ the block column's global z-coordinate * @param cubeY the y-coordinate of the cube underneath which the highest occluding block is to be found * * @return the y-coordinate of the highest occluding block underneath the given cube in the specified block column * or {@link #DEFAULT_OCCLUSION_HEIGHT} if no such block exists */ private static int getOcclusionHeightBelowCubeY(Column column, int blockX, int blockZ, int cubeY) { IHeightMap index = column.getOpacityIndex(); return index.getTopBlockYBelow(blockToLocal(blockX), blockToLocal(blockZ), cubeToMinBlock(cubeY)); } /** * Determines which vertical section of the specified block column in the given cube requires a lighting update * based on the current occlusion in the cube's column. * * @param cube the cube inside of which the skylight is to be updated * @param localX the local x-coordinate of the block column * @param localZ the local z-coordinate of the block column * * @return a pair containing the minimum and the maximum y-coordinate to be updated in the given cube */ private static ImmutablePair<Integer, Integer> getMinMaxLightUpdateY(Cube cube, int localX, int localZ) { Column column = cube.getColumn(); int heightMax = getOcclusionHeight(column, localX, localZ);//==Y of the top block // If the given cube is above the highest occluding block in the column, everything is fully lit. int cubeY = cube.getY(); if (blockToCube(heightMax) < cubeY) { return null; } int blockX = cubeToMinBlock(cube.getX()) + localX; int blockZ = cubeToMinBlock(cube.getZ()) + localZ; // If the given cube lies underneath the occluding block, it must be updated from the top down. if (cubeY < blockToCube(heightMax)) { // Determine the y-coordinate of the highest block (and its cube) occluding blocks inside of the given cube // or further down. int topBlockYInThisCubeOrBelow = getOcclusionHeightBelowCubeY(column, blockX, blockZ, cube.getY() + 1); int topBlockCubeYInThisCubeOrBelow = blockToCube(topBlockYInThisCubeOrBelow); // If the given cube contains the occluding block, the update can be limited down to that block. if (topBlockCubeYInThisCubeOrBelow == cubeY) { int heightBelowCube = getOcclusionHeightBelowCubeY(column, blockX, blockZ, cube.getY()) + 1; return new ImmutablePair<>(heightBelowCube, cubeToMaxBlock(cubeY)); } // Otherwise, the whole height of the cube must be updated. else { return new ImmutablePair<>(cubeToMinBlock(cubeY), cubeToMaxBlock(cubeY)); } } // ... otherwise, the update must start at the occluding block. int heightBelowCube = getOcclusionHeightBelowCubeY(column, blockX, blockZ, cubeY); return new ImmutablePair<>(heightBelowCube, heightMax); } }