mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2024-12-29 08:26:37 +01:00
Moving light updates
- Better system for moving objects that want to receive light updates - LightProvider interface to better abstract light lookups - All light listeners use GridAlignedBBs - More utility in GridAlignedBB
This commit is contained in:
parent
7ca4ea5c3e
commit
fe304041c5
19 changed files with 191 additions and 168 deletions
|
@ -4,6 +4,7 @@ import com.jozufozu.flywheel.config.FlwCommands;
|
||||||
import com.jozufozu.flywheel.config.FlwConfig;
|
import com.jozufozu.flywheel.config.FlwConfig;
|
||||||
import com.jozufozu.flywheel.config.FlwPackets;
|
import com.jozufozu.flywheel.config.FlwPackets;
|
||||||
|
|
||||||
|
import net.minecraft.util.ResourceLocation;
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
import net.minecraftforge.common.MinecraftForge;
|
import net.minecraftforge.common.MinecraftForge;
|
||||||
import net.minecraftforge.fml.DistExecutor;
|
import net.minecraftforge.fml.DistExecutor;
|
||||||
|
@ -28,6 +29,10 @@ public class Flywheel {
|
||||||
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> FlywheelClient::clientInit);
|
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> FlywheelClient::clientInit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation rl(String path) {
|
||||||
|
return new ResourceLocation(ID, path);
|
||||||
|
}
|
||||||
|
|
||||||
private void setup(final FMLCommonSetupEvent event) {
|
private void setup(final FMLCommonSetupEvent event) {
|
||||||
FlwPackets.registerPackets();
|
FlwPackets.registerPackets();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.jozufozu.flywheel.core.AtlasStitcher;
|
||||||
import com.jozufozu.flywheel.core.Contexts;
|
import com.jozufozu.flywheel.core.Contexts;
|
||||||
import com.jozufozu.flywheel.core.Materials;
|
import com.jozufozu.flywheel.core.Materials;
|
||||||
import com.jozufozu.flywheel.core.PartialModel;
|
import com.jozufozu.flywheel.core.PartialModel;
|
||||||
|
import com.jozufozu.flywheel.light.debug.DebugView;
|
||||||
import com.jozufozu.flywheel.vanilla.VanillaInstances;
|
import com.jozufozu.flywheel.vanilla.VanillaInstances;
|
||||||
|
|
||||||
import net.minecraftforge.eventbus.api.IEventBus;
|
import net.minecraftforge.eventbus.api.IEventBus;
|
||||||
|
|
|
@ -8,11 +8,10 @@ import com.jozufozu.flywheel.backend.material.MaterialManager;
|
||||||
import com.jozufozu.flywheel.core.materials.IFlatLight;
|
import com.jozufozu.flywheel.core.materials.IFlatLight;
|
||||||
import com.jozufozu.flywheel.light.GridAlignedBB;
|
import com.jozufozu.flywheel.light.GridAlignedBB;
|
||||||
import com.jozufozu.flywheel.light.ILightUpdateListener;
|
import com.jozufozu.flywheel.light.ILightUpdateListener;
|
||||||
import com.jozufozu.flywheel.light.LightUpdater;
|
import com.jozufozu.flywheel.light.LightProvider;
|
||||||
import com.jozufozu.flywheel.light.ListenerStatus;
|
import com.jozufozu.flywheel.light.ListenerStatus;
|
||||||
|
|
||||||
import net.minecraft.util.math.BlockPos;
|
import net.minecraft.util.math.BlockPos;
|
||||||
import net.minecraft.world.IBlockDisplayReader;
|
|
||||||
import net.minecraft.world.LightType;
|
import net.minecraft.world.LightType;
|
||||||
import net.minecraft.world.World;
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ import net.minecraft.world.World;
|
||||||
public abstract class AbstractInstance implements IInstance, ILightUpdateListener {
|
public abstract class AbstractInstance implements IInstance, ILightUpdateListener {
|
||||||
|
|
||||||
protected final MaterialManager materialManager;
|
protected final MaterialManager materialManager;
|
||||||
protected final World world;
|
public final World world;
|
||||||
|
|
||||||
public AbstractInstance(MaterialManager materialManager, World world) {
|
public AbstractInstance(MaterialManager materialManager, World world) {
|
||||||
this.materialManager = materialManager;
|
this.materialManager = materialManager;
|
||||||
|
@ -74,7 +73,7 @@ public abstract class AbstractInstance implements IInstance, ILightUpdateListene
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLightUpdate(IBlockDisplayReader world, LightType type, GridAlignedBB changed) {
|
public void onLightUpdate(LightProvider world, LightType type, GridAlignedBB changed) {
|
||||||
updateLight();
|
updateLight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import javax.annotation.Nullable;
|
||||||
import com.jozufozu.flywheel.backend.Backend;
|
import com.jozufozu.flywheel.backend.Backend;
|
||||||
import com.jozufozu.flywheel.backend.material.MaterialManager;
|
import com.jozufozu.flywheel.backend.material.MaterialManager;
|
||||||
import com.jozufozu.flywheel.backend.material.MaterialManagerImpl;
|
import com.jozufozu.flywheel.backend.material.MaterialManagerImpl;
|
||||||
|
import com.jozufozu.flywheel.light.LightUpdater;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
import net.minecraft.client.renderer.ActiveRenderInfo;
|
import net.minecraft.client.renderer.ActiveRenderInfo;
|
||||||
|
@ -285,7 +286,7 @@ public abstract class InstanceManager<T> implements MaterialManagerImpl.OriginSh
|
||||||
|
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
renderer.updateLight();
|
renderer.updateLight();
|
||||||
renderer.startListening();
|
LightUpdater.get(renderer.world).addListener(renderer);
|
||||||
instances.put(obj, renderer);
|
instances.put(obj, renderer);
|
||||||
|
|
||||||
if (renderer instanceof IDynamicInstance) dynamicInstances.put(obj, (IDynamicInstance) renderer);
|
if (renderer instanceof IDynamicInstance) dynamicInstances.put(obj, (IDynamicInstance) renderer);
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
package com.jozufozu.flywheel.backend.instancing.entity;
|
package com.jozufozu.flywheel.backend.instancing.entity;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import com.jozufozu.flywheel.backend.instancing.IDynamicInstance;
|
import com.jozufozu.flywheel.backend.instancing.IDynamicInstance;
|
||||||
import com.jozufozu.flywheel.backend.instancing.AbstractInstance;
|
import com.jozufozu.flywheel.backend.instancing.AbstractInstance;
|
||||||
import com.jozufozu.flywheel.backend.instancing.ITickableInstance;
|
import com.jozufozu.flywheel.backend.instancing.ITickableInstance;
|
||||||
import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager;
|
import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager;
|
||||||
import com.jozufozu.flywheel.backend.material.MaterialManager;
|
import com.jozufozu.flywheel.backend.material.MaterialManager;
|
||||||
|
import com.jozufozu.flywheel.light.GridAlignedBB;
|
||||||
import com.jozufozu.flywheel.light.ILightUpdateListener;
|
import com.jozufozu.flywheel.light.ILightUpdateListener;
|
||||||
import com.jozufozu.flywheel.light.ListenerStatus;
|
import com.jozufozu.flywheel.light.IMovingListener;
|
||||||
import com.jozufozu.flywheel.light.Volume;
|
import com.jozufozu.flywheel.light.LightProvider;
|
||||||
|
|
||||||
import net.minecraft.entity.Entity;
|
import net.minecraft.entity.Entity;
|
||||||
import net.minecraft.tileentity.TileEntity;
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
import net.minecraft.util.math.AxisAlignedBB;
|
||||||
import net.minecraft.util.math.BlockPos;
|
import net.minecraft.util.math.BlockPos;
|
||||||
import net.minecraft.util.math.MathHelper;
|
import net.minecraft.util.math.MathHelper;
|
||||||
import net.minecraft.util.math.vector.Vector3d;
|
import net.minecraft.util.math.vector.Vector3d;
|
||||||
|
@ -35,25 +34,33 @@ import net.minecraft.util.math.vector.Vector3i;
|
||||||
*
|
*
|
||||||
* @param <E> The type of {@link Entity} your class is an instance of.
|
* @param <E> The type of {@link Entity} your class is an instance of.
|
||||||
*/
|
*/
|
||||||
public abstract class EntityInstance<E extends Entity> extends AbstractInstance implements ILightUpdateListener {
|
public abstract class EntityInstance<E extends Entity> extends AbstractInstance implements ILightUpdateListener, IMovingListener {
|
||||||
|
|
||||||
protected final E entity;
|
protected final E entity;
|
||||||
|
protected final GridAlignedBB bounds;
|
||||||
|
|
||||||
public EntityInstance(MaterialManager materialManager, E entity) {
|
public EntityInstance(MaterialManager materialManager, E entity) {
|
||||||
super(materialManager, entity.level);
|
super(materialManager, entity.level);
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
|
bounds = GridAlignedBB.from(entity.getBoundingBox());
|
||||||
startListening();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Volume.Box getVolume() {
|
public GridAlignedBB getVolume() {
|
||||||
return Volume.box(entity.getBoundingBox());
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenerStatus status() {
|
public boolean update(LightProvider provider) {
|
||||||
return ListenerStatus.UPDATE;
|
AxisAlignedBB boundsNow = entity.getBoundingBox();
|
||||||
|
|
||||||
|
if (bounds.sameAs(boundsNow)) return false;
|
||||||
|
|
||||||
|
bounds.assign(boundsNow);
|
||||||
|
|
||||||
|
updateLight();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,7 +9,6 @@ import com.jozufozu.flywheel.core.Materials;
|
||||||
import com.jozufozu.flywheel.core.materials.ModelData;
|
import com.jozufozu.flywheel.core.materials.ModelData;
|
||||||
import com.jozufozu.flywheel.core.materials.OrientedData;
|
import com.jozufozu.flywheel.core.materials.OrientedData;
|
||||||
import com.jozufozu.flywheel.light.GridAlignedBB;
|
import com.jozufozu.flywheel.light.GridAlignedBB;
|
||||||
import com.jozufozu.flywheel.light.Volume;
|
|
||||||
|
|
||||||
import net.minecraft.block.BlockState;
|
import net.minecraft.block.BlockState;
|
||||||
import net.minecraft.tileentity.TileEntity;
|
import net.minecraft.tileentity.TileEntity;
|
||||||
|
@ -85,7 +84,7 @@ public abstract class TileEntityInstance<T extends TileEntity> extends AbstractI
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Volume.Block getVolume() {
|
public GridAlignedBB getVolume() {
|
||||||
return Volume.block(pos);
|
return GridAlignedBB.from(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import net.minecraft.client.world.ClientWorld;
|
||||||
import net.minecraft.world.IWorld;
|
import net.minecraft.world.IWorld;
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||||
import net.minecraftforge.client.event.RenderWorldLastEvent;
|
|
||||||
import net.minecraftforge.event.TickEvent;
|
import net.minecraftforge.event.TickEvent;
|
||||||
import net.minecraftforge.event.world.WorldEvent;
|
import net.minecraftforge.event.world.WorldEvent;
|
||||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
@ -51,7 +50,7 @@ public class ForgeEvents {
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void rwle(TickEvent.ClientTickEvent e) {
|
public static void rwle(TickEvent.ClientTickEvent e) {
|
||||||
if (e.phase == TickEvent.Phase.END && Backend.isGameActive())
|
if (e.phase == TickEvent.Phase.END && Backend.isGameActive())
|
||||||
LightUpdater.getInstance().tick();
|
LightUpdater.get(Minecraft.getInstance().level).tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
30
src/main/java/com/jozufozu/flywheel/light/BasicProvider.java
Normal file
30
src/main/java/com/jozufozu/flywheel/light/BasicProvider.java
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package com.jozufozu.flywheel.light;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.world.IBlockDisplayReader;
|
||||||
|
import net.minecraft.world.LightType;
|
||||||
|
|
||||||
|
public class BasicProvider implements LightProvider {
|
||||||
|
|
||||||
|
private static final Map<IBlockDisplayReader, BasicProvider> wrappers = new WeakHashMap<>();
|
||||||
|
|
||||||
|
public static BasicProvider get(IBlockDisplayReader world) {
|
||||||
|
return wrappers.computeIfAbsent(world, BasicProvider::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final BlockPos.Mutable pos = new BlockPos.Mutable();
|
||||||
|
|
||||||
|
private final IBlockDisplayReader reader;
|
||||||
|
|
||||||
|
public BasicProvider(IBlockDisplayReader reader) {
|
||||||
|
this.reader = reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLight(LightType type, int x, int y, int z) {
|
||||||
|
return reader.getBrightness(type, pos.set(x, y, z));
|
||||||
|
}
|
||||||
|
}
|
|
@ -75,6 +75,15 @@ public class GridAlignedBB {
|
||||||
return minX == other.minX && minY == other.minY && minZ == other.minZ && maxX == other.maxX && maxY == other.maxY && maxZ == other.maxZ;
|
return minX == other.minX && minY == other.minY && minZ == other.minZ && maxX == other.maxX && maxY == other.maxY && maxZ == other.maxZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean sameAs(AxisAlignedBB other) {
|
||||||
|
return minX == Math.floor(other.minX)
|
||||||
|
&& minY == Math.floor(other.minY)
|
||||||
|
&& minZ == Math.floor(other.minZ)
|
||||||
|
&& maxX == Math.ceil(other.maxX)
|
||||||
|
&& maxY == Math.ceil(other.maxY)
|
||||||
|
&& maxZ == Math.ceil(other.maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
public void fixMinMax() {
|
public void fixMinMax() {
|
||||||
int minX = Math.min(this.minX, this.maxX);
|
int minX = Math.min(this.minX, this.maxX);
|
||||||
int minY = Math.min(this.minY, this.maxY);
|
int minY = Math.min(this.minY, this.maxY);
|
||||||
|
@ -245,6 +254,24 @@ public class GridAlignedBB {
|
||||||
this.maxZ = Math.max(this.maxZ, (int) Math.ceil(other.maxZ));
|
this.maxZ = Math.max(this.maxZ, (int) Math.ceil(other.maxZ));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void assign(AxisAlignedBB other) {
|
||||||
|
this.minX = (int) Math.floor(other.minX);
|
||||||
|
this.minY = (int) Math.floor(other.minY);
|
||||||
|
this.minZ = (int) Math.floor(other.minZ);
|
||||||
|
this.maxX = (int) Math.ceil(other.maxX);
|
||||||
|
this.maxY = (int) Math.ceil(other.maxY);
|
||||||
|
this.maxZ = (int) Math.ceil(other.maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assign(GridAlignedBB other) {
|
||||||
|
this.minX = other.minX;
|
||||||
|
this.minY = other.minY;
|
||||||
|
this.minZ = other.minZ;
|
||||||
|
this.maxX = other.maxX;
|
||||||
|
this.maxY = other.maxY;
|
||||||
|
this.maxZ = other.maxZ;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean intersects(GridAlignedBB other) {
|
public boolean intersects(GridAlignedBB other) {
|
||||||
return this.intersects(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ);
|
return this.intersects(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ);
|
||||||
}
|
}
|
||||||
|
@ -297,5 +324,4 @@ public class GridAlignedBB {
|
||||||
result = 31 * result + maxZ;
|
result = 31 * result + maxZ;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,23 @@
|
||||||
package com.jozufozu.flywheel.light;
|
package com.jozufozu.flywheel.light;
|
||||||
|
|
||||||
import net.minecraft.world.IBlockDisplayReader;
|
|
||||||
import net.minecraft.world.LightType;
|
import net.minecraft.world.LightType;
|
||||||
|
|
||||||
/**
|
|
||||||
* Anything can implement this, implementors should call {@link #startListening}
|
|
||||||
* appropriately to make sure they get the updates they want.
|
|
||||||
*/
|
|
||||||
public interface ILightUpdateListener {
|
public interface ILightUpdateListener {
|
||||||
|
|
||||||
Volume getVolume();
|
GridAlignedBB getVolume();
|
||||||
|
|
||||||
ListenerStatus status();
|
ListenerStatus status();
|
||||||
|
|
||||||
default void startListening() {
|
|
||||||
LightUpdater.getInstance().addListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a light updates in a chunk the implementor cares about.
|
* Called when a light updates in a chunk the implementor cares about.
|
||||||
*/
|
*/
|
||||||
void onLightUpdate(IBlockDisplayReader world, LightType type, GridAlignedBB changed);
|
void onLightUpdate(LightProvider world, LightType type, GridAlignedBB changed);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the server sends light data to the client.
|
* Called when the server sends light data to the client.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
default void onLightPacket(IBlockDisplayReader world, int chunkX, int chunkZ) {
|
default void onLightPacket(LightProvider world, int chunkX, int chunkZ) {
|
||||||
GridAlignedBB changedVolume = GridAlignedBB.from(chunkX, chunkZ);
|
GridAlignedBB changedVolume = GridAlignedBB.from(chunkX, chunkZ);
|
||||||
|
|
||||||
onLightUpdate(world, LightType.BLOCK, changedVolume);
|
onLightUpdate(world, LightType.BLOCK, changedVolume);
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.jozufozu.flywheel.light;
|
||||||
|
|
||||||
|
public interface IMovingListener extends ILightUpdateListener {
|
||||||
|
boolean update(LightProvider provider);
|
||||||
|
}
|
12
src/main/java/com/jozufozu/flywheel/light/LightProvider.java
Normal file
12
src/main/java/com/jozufozu/flywheel/light/LightProvider.java
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package com.jozufozu.flywheel.light;
|
||||||
|
|
||||||
|
import net.minecraft.client.renderer.LightTexture;
|
||||||
|
import net.minecraft.world.LightType;
|
||||||
|
|
||||||
|
public interface LightProvider {
|
||||||
|
int getLight(LightType type, int x, int y, int z);
|
||||||
|
|
||||||
|
default int getPackedLight(int x, int y, int z) {
|
||||||
|
return LightTexture.pack(getLight(LightType.BLOCK, x, y, z), getLight(LightType.SKY, x, y, z));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +1,42 @@
|
||||||
package com.jozufozu.flywheel.light;
|
package com.jozufozu.flywheel.light;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.util.WeakHashSet;
|
import com.jozufozu.flywheel.util.WeakHashSet;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||||
import net.minecraft.client.Minecraft;
|
|
||||||
import net.minecraft.util.math.BlockPos;
|
import net.minecraft.util.math.BlockPos;
|
||||||
import net.minecraft.util.math.SectionPos;
|
import net.minecraft.util.math.SectionPos;
|
||||||
import net.minecraft.world.IBlockDisplayReader;
|
import net.minecraft.world.IBlockDisplayReader;
|
||||||
import net.minecraft.world.LightType;
|
import net.minecraft.world.LightType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of what chunks/sections each listener is in so we can update exactly what needs to be updated.
|
||||||
|
*/
|
||||||
public class LightUpdater {
|
public class LightUpdater {
|
||||||
|
|
||||||
private static LightUpdater instance;
|
private static final Map<IBlockDisplayReader, LightUpdater> light = new HashMap<>();
|
||||||
|
public static LightUpdater get(IBlockDisplayReader world) {
|
||||||
public static LightUpdater getInstance() {
|
return light.computeIfAbsent(world, LightUpdater::new);
|
||||||
if (instance == null) instance = new LightUpdater();
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final WeakHashSet<ILightUpdateListener> allListeners;
|
private final LightProvider provider;
|
||||||
private final WeakContainmentMultiMap<ILightUpdateListener> sections;
|
|
||||||
private final WeakContainmentMultiMap<ILightUpdateListener> chunks;
|
|
||||||
|
|
||||||
public LightUpdater() {
|
private final WeakHashSet<IMovingListener> movingListeners = new WeakHashSet<>();
|
||||||
allListeners = new WeakHashSet<>();
|
private final WeakContainmentMultiMap<ILightUpdateListener> sections = new WeakContainmentMultiMap<>();
|
||||||
sections = new WeakContainmentMultiMap<>();
|
private final WeakContainmentMultiMap<ILightUpdateListener> chunks = new WeakContainmentMultiMap<>();
|
||||||
chunks = new WeakContainmentMultiMap<>();
|
|
||||||
|
public LightUpdater(IBlockDisplayReader world) {
|
||||||
|
provider = BasicProvider.get(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tick() {
|
public void tick() {
|
||||||
for (ILightUpdateListener listener : allListeners) {
|
for (IMovingListener listener : movingListeners) {
|
||||||
if (listener.status() == ListenerStatus.UPDATE) {
|
if (listener.update(provider)) {
|
||||||
addListener(listener);
|
addListener(listener);
|
||||||
|
|
||||||
listener.onLightUpdate(Minecraft.getInstance().level, LightType.BLOCK, null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,25 +47,14 @@ public class LightUpdater {
|
||||||
* @param listener The object that wants to receive light update notifications.
|
* @param listener The object that wants to receive light update notifications.
|
||||||
*/
|
*/
|
||||||
public void addListener(ILightUpdateListener listener) {
|
public void addListener(ILightUpdateListener listener) {
|
||||||
allListeners.add(listener);
|
if (listener instanceof IMovingListener)
|
||||||
|
movingListeners.add(((IMovingListener) listener));
|
||||||
|
|
||||||
Volume volume = listener.getVolume();
|
GridAlignedBB box = listener.getVolume();
|
||||||
|
|
||||||
LongSet sections = this.sections.getAndResetContainment(listener);
|
LongSet sections = this.sections.getAndResetContainment(listener);
|
||||||
LongSet chunks = this.chunks.getAndResetContainment(listener);
|
LongSet chunks = this.chunks.getAndResetContainment(listener);
|
||||||
|
|
||||||
if (volume instanceof Volume.Block) {
|
|
||||||
BlockPos pos = ((Volume.Block) volume).pos;
|
|
||||||
long sectionPos = blockToSection(pos);
|
|
||||||
this.sections.put(sectionPos, listener);
|
|
||||||
sections.add(sectionPos);
|
|
||||||
|
|
||||||
long chunkPos = sectionToChunk(sectionPos);
|
|
||||||
this.chunks.put(chunkPos, listener);
|
|
||||||
chunks.add(chunkPos);
|
|
||||||
} else if (volume instanceof Volume.Box) {
|
|
||||||
GridAlignedBB box = ((Volume.Box) volume).box;
|
|
||||||
|
|
||||||
int minX = SectionPos.blockToSectionCoord(box.minX);
|
int minX = SectionPos.blockToSectionCoord(box.minX);
|
||||||
int minY = SectionPos.blockToSectionCoord(box.minY);
|
int minY = SectionPos.blockToSectionCoord(box.minY);
|
||||||
int minZ = SectionPos.blockToSectionCoord(box.minZ);
|
int minZ = SectionPos.blockToSectionCoord(box.minZ);
|
||||||
|
@ -86,16 +75,13 @@ public class LightUpdater {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch light updates to all registered {@link ILightUpdateListener}s.
|
* Dispatch light updates to all registered {@link ILightUpdateListener}s.
|
||||||
*
|
|
||||||
* @param world The world in which light was updated.
|
|
||||||
* @param type The type of light that changed.
|
* @param type The type of light that changed.
|
||||||
* @param sectionPos A long representing the section position where light changed.
|
* @param sectionPos A long representing the section position where light changed.
|
||||||
*/
|
*/
|
||||||
public void onLightUpdate(IBlockDisplayReader world, LightType type, long sectionPos) {
|
public void onLightUpdate(LightType type, long sectionPos) {
|
||||||
Set<ILightUpdateListener> set = sections.get(sectionPos);
|
Set<ILightUpdateListener> set = sections.get(sectionPos);
|
||||||
|
|
||||||
if (set == null || set.isEmpty()) return;
|
if (set == null || set.isEmpty()) return;
|
||||||
|
@ -105,7 +91,7 @@ public class LightUpdater {
|
||||||
GridAlignedBB chunkBox = GridAlignedBB.from(SectionPos.of(sectionPos));
|
GridAlignedBB chunkBox = GridAlignedBB.from(SectionPos.of(sectionPos));
|
||||||
|
|
||||||
for (ILightUpdateListener listener : set) {
|
for (ILightUpdateListener listener : set) {
|
||||||
listener.onLightUpdate(world, type, chunkBox.copy());
|
listener.onLightUpdate(provider, type, chunkBox.copy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,9 +99,8 @@ public class LightUpdater {
|
||||||
* Dispatch light updates to all registered {@link ILightUpdateListener}s
|
* Dispatch light updates to all registered {@link ILightUpdateListener}s
|
||||||
* when the server sends lighting data for an entire chunk.
|
* when the server sends lighting data for an entire chunk.
|
||||||
*
|
*
|
||||||
* @param world The world in which light was updated.
|
|
||||||
*/
|
*/
|
||||||
public void onLightPacket(IBlockDisplayReader world, int chunkX, int chunkZ) {
|
public void onLightPacket(int chunkX, int chunkZ) {
|
||||||
long chunkPos = SectionPos.asLong(chunkX, 0, chunkZ);
|
long chunkPos = SectionPos.asLong(chunkX, 0, chunkZ);
|
||||||
|
|
||||||
Set<ILightUpdateListener> set = chunks.get(chunkPos);
|
Set<ILightUpdateListener> set = chunks.get(chunkPos);
|
||||||
|
@ -125,7 +110,7 @@ public class LightUpdater {
|
||||||
set.removeIf(l -> l.status().shouldRemove());
|
set.removeIf(l -> l.status().shouldRemove());
|
||||||
|
|
||||||
for (ILightUpdateListener listener : set) {
|
for (ILightUpdateListener listener : set) {
|
||||||
listener.onLightPacket(world, chunkX, chunkZ);
|
listener.onLightPacket(provider, chunkX, chunkZ);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,4 +121,12 @@ public class LightUpdater {
|
||||||
public static long sectionToChunk(long sectionPos) {
|
public static long sectionToChunk(long sectionPos) {
|
||||||
return sectionPos & 0xFFFFFFFFFFF_00000L;
|
return sectionPos & 0xFFFFFFFFFFF_00000L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Stream<GridAlignedBB> getAllBoxes() {
|
||||||
|
return chunks.stream().map(ILightUpdateListener::getVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return chunks.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,11 +124,12 @@ public class LightVolume {
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
|
|
||||||
if (textureVolume.contains(newSampleVolume)) {
|
if (textureVolume.contains(newSampleVolume)) {
|
||||||
|
BasicProvider basicProvider = BasicProvider.get(world);
|
||||||
if (newSampleVolume.intersects(sampleVolume)) {
|
if (newSampleVolume.intersects(sampleVolume)) {
|
||||||
GridAlignedBB newArea = newSampleVolume.intersect(sampleVolume);
|
GridAlignedBB newArea = newSampleVolume.intersect(sampleVolume);
|
||||||
sampleVolume = newSampleVolume;
|
sampleVolume = newSampleVolume;
|
||||||
|
|
||||||
copyLight(world, newArea);
|
copyLight(basicProvider, newArea);
|
||||||
} else {
|
} else {
|
||||||
sampleVolume = newSampleVolume;
|
sampleVolume = newSampleVolume;
|
||||||
initialize(world);
|
initialize(world);
|
||||||
|
@ -143,7 +144,7 @@ public class LightVolume {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyLightUpdate(IBlockDisplayReader world, LightType type, GridAlignedBB changedVolume) {
|
public void notifyLightUpdate(LightProvider world, LightType type, GridAlignedBB changedVolume) {
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
|
|
||||||
if (!changedVolume.intersects(sampleVolume)) return;
|
if (!changedVolume.intersects(sampleVolume)) return;
|
||||||
|
@ -153,7 +154,7 @@ public class LightVolume {
|
||||||
else if (type == LightType.SKY) copySky(world, changedVolume);
|
else if (type == LightType.SKY) copySky(world, changedVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void notifyLightPacket(IBlockDisplayReader world, int chunkX, int chunkZ) {
|
public void notifyLightPacket(LightProvider world, int chunkX, int chunkZ) {
|
||||||
if (removed) return;
|
if (removed) return;
|
||||||
|
|
||||||
GridAlignedBB changedVolume = GridAlignedBB.from(chunkX, chunkZ);
|
GridAlignedBB changedVolume = GridAlignedBB.from(chunkX, chunkZ);
|
||||||
|
@ -193,17 +194,13 @@ public class LightVolume {
|
||||||
*
|
*
|
||||||
* @param worldVolume the region in the world to copy data from.
|
* @param worldVolume the region in the world to copy data from.
|
||||||
*/
|
*/
|
||||||
public void copyBlock(IBlockDisplayReader world, GridAlignedBB worldVolume) {
|
public void copyBlock(LightProvider world, GridAlignedBB worldVolume) {
|
||||||
BlockPos.Mutable pos = new BlockPos.Mutable();
|
|
||||||
|
|
||||||
int xShift = textureVolume.minX;
|
int xShift = textureVolume.minX;
|
||||||
int yShift = textureVolume.minY;
|
int yShift = textureVolume.minY;
|
||||||
int zShift = textureVolume.minZ;
|
int zShift = textureVolume.minZ;
|
||||||
|
|
||||||
worldVolume.forEachContained((x, y, z) -> {
|
worldVolume.forEachContained((x, y, z) -> {
|
||||||
pos.set(x, y, z);
|
int light = world.getLight(LightType.BLOCK, x, y, z);
|
||||||
|
|
||||||
int light = world.getBrightness(LightType.BLOCK, pos);
|
|
||||||
|
|
||||||
writeBlock(x - xShift, y - yShift, z - zShift, light);
|
writeBlock(x - xShift, y - yShift, z - zShift, light);
|
||||||
});
|
});
|
||||||
|
@ -216,17 +213,13 @@ public class LightVolume {
|
||||||
*
|
*
|
||||||
* @param worldVolume the region in the world to copy data from.
|
* @param worldVolume the region in the world to copy data from.
|
||||||
*/
|
*/
|
||||||
public void copySky(IBlockDisplayReader world, GridAlignedBB worldVolume) {
|
public void copySky(LightProvider world, GridAlignedBB worldVolume) {
|
||||||
BlockPos.Mutable pos = new BlockPos.Mutable();
|
|
||||||
|
|
||||||
int xShift = textureVolume.minX;
|
int xShift = textureVolume.minX;
|
||||||
int yShift = textureVolume.minY;
|
int yShift = textureVolume.minY;
|
||||||
int zShift = textureVolume.minZ;
|
int zShift = textureVolume.minZ;
|
||||||
|
|
||||||
worldVolume.forEachContained((x, y, z) -> {
|
worldVolume.forEachContained((x, y, z) -> {
|
||||||
pos.set(x, y, z);
|
int light = world.getLight(LightType.SKY, x, y, z);
|
||||||
|
|
||||||
int light = world.getBrightness(LightType.SKY, pos);
|
|
||||||
|
|
||||||
writeSky(x - xShift, y - yShift, z - zShift, light);
|
writeSky(x - xShift, y - yShift, z - zShift, light);
|
||||||
});
|
});
|
||||||
|
@ -239,7 +232,7 @@ public class LightVolume {
|
||||||
*
|
*
|
||||||
* @param worldVolume the region in the world to copy data from.
|
* @param worldVolume the region in the world to copy data from.
|
||||||
*/
|
*/
|
||||||
public void copyLight(IBlockDisplayReader world, GridAlignedBB worldVolume) {
|
public void copyLight(LightProvider world, GridAlignedBB worldVolume) {
|
||||||
BlockPos.Mutable pos = new BlockPos.Mutable();
|
BlockPos.Mutable pos = new BlockPos.Mutable();
|
||||||
|
|
||||||
int xShift = textureVolume.minX;
|
int xShift = textureVolume.minX;
|
||||||
|
@ -249,8 +242,8 @@ public class LightVolume {
|
||||||
worldVolume.forEachContained((x, y, z) -> {
|
worldVolume.forEachContained((x, y, z) -> {
|
||||||
pos.set(x, y, z);
|
pos.set(x, y, z);
|
||||||
|
|
||||||
int block = world.getBrightness(LightType.BLOCK, pos);
|
int block = world.getLight(LightType.BLOCK, x, y, z);
|
||||||
int sky = world.getBrightness(LightType.SKY, pos);
|
int sky = world.getLight(LightType.SKY, x, y, z);
|
||||||
|
|
||||||
writeLight(x - xShift, y - yShift, z - zShift, block, sky);
|
writeLight(x - xShift, y - yShift, z - zShift, block, sky);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
package com.jozufozu.flywheel.light;
|
|
||||||
|
|
||||||
import net.minecraft.util.math.AxisAlignedBB;
|
|
||||||
import net.minecraft.util.math.BlockPos;
|
|
||||||
|
|
||||||
public class Volume {
|
|
||||||
|
|
||||||
public static Volume.Block block(BlockPos pos) {
|
|
||||||
return new Block(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Volume.Box box(GridAlignedBB box) {
|
|
||||||
return new Box(box);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Volume.Box box(AxisAlignedBB box) {
|
|
||||||
return new Box(GridAlignedBB.from(box));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Block extends Volume {
|
|
||||||
public final BlockPos pos;
|
|
||||||
|
|
||||||
public Block(BlockPos pos) {
|
|
||||||
this.pos = pos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Box extends Volume {
|
|
||||||
public final GridAlignedBB box;
|
|
||||||
|
|
||||||
public Box(GridAlignedBB box) {
|
|
||||||
this.box = box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,8 @@
|
||||||
package com.jozufozu.flywheel.light;
|
package com.jozufozu.flywheel.light;
|
||||||
|
|
||||||
|
import java.util.AbstractCollection;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.function.LongConsumer;
|
import java.util.function.LongConsumer;
|
||||||
|
@ -11,7 +14,7 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.longs.LongRBTreeSet;
|
import it.unimi.dsi.fastutil.longs.LongRBTreeSet;
|
||||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||||
|
|
||||||
public class WeakContainmentMultiMap<T> {
|
public class WeakContainmentMultiMap<T> extends AbstractCollection<T> {
|
||||||
|
|
||||||
private final Long2ObjectMap<WeakHashSet<T>> forward;
|
private final Long2ObjectMap<WeakHashSet<T>> forward;
|
||||||
private final WeakHashMap<T, LongSet> reverse;
|
private final WeakHashMap<T, LongSet> reverse;
|
||||||
|
@ -53,4 +56,14 @@ public class WeakContainmentMultiMap<T> {
|
||||||
public void put(long sectionPos, T listener) {
|
public void put(long sectionPos, T listener) {
|
||||||
forward.computeIfAbsent(sectionPos, $ -> new WeakHashSet<>()).add(listener);
|
forward.computeIfAbsent(sectionPos, $ -> new WeakHashSet<>()).add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<T> iterator() {
|
||||||
|
return reverse.keySet().iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return reverse.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
package com.jozufozu.flywheel.mixin.light;
|
package com.jozufozu.flywheel.mixin.light;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.backend.instancing.InstanceManager;
|
|
||||||
import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher;
|
|
||||||
import com.jozufozu.flywheel.light.LightUpdater;
|
import com.jozufozu.flywheel.light.LightUpdater;
|
||||||
import com.jozufozu.flywheel.util.ChunkUtil;
|
|
||||||
|
|
||||||
import net.minecraft.client.multiplayer.ClientChunkProvider;
|
import net.minecraft.client.multiplayer.ClientChunkProvider;
|
||||||
import net.minecraft.client.world.ClientWorld;
|
import net.minecraft.client.world.ClientWorld;
|
||||||
import net.minecraft.entity.Entity;
|
|
||||||
import net.minecraft.tileentity.TileEntity;
|
|
||||||
import net.minecraft.util.math.SectionPos;
|
import net.minecraft.util.math.SectionPos;
|
||||||
import net.minecraft.world.LightType;
|
import net.minecraft.world.LightType;
|
||||||
import net.minecraft.world.chunk.AbstractChunkProvider;
|
import net.minecraft.world.chunk.AbstractChunkProvider;
|
||||||
import net.minecraft.world.chunk.Chunk;
|
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
|
||||||
|
@ -38,7 +30,7 @@ public abstract class LightUpdateMixin extends AbstractChunkProvider {
|
||||||
ClientChunkProvider thi = ((ClientChunkProvider) (Object) this);
|
ClientChunkProvider thi = ((ClientChunkProvider) (Object) this);
|
||||||
ClientWorld world = (ClientWorld) thi.getLevel();
|
ClientWorld world = (ClientWorld) thi.getLevel();
|
||||||
|
|
||||||
LightUpdater.getInstance()
|
LightUpdater.get(world)
|
||||||
.onLightUpdate(world, type, pos.asLong());
|
.onLightUpdate(type, pos.asLong());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
package com.jozufozu.flywheel.mixin.light;
|
package com.jozufozu.flywheel.mixin.light;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.backend.RenderWork;
|
import com.jozufozu.flywheel.backend.RenderWork;
|
||||||
import com.jozufozu.flywheel.backend.instancing.InstanceManager;
|
|
||||||
import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher;
|
|
||||||
import com.jozufozu.flywheel.light.LightUpdater;
|
import com.jozufozu.flywheel.light.LightUpdater;
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.network.play.ClientPlayNetHandler;
|
import net.minecraft.client.network.play.ClientPlayNetHandler;
|
||||||
import net.minecraft.client.world.ClientWorld;
|
import net.minecraft.client.world.ClientWorld;
|
||||||
import net.minecraft.entity.Entity;
|
|
||||||
import net.minecraft.network.play.server.SUpdateLightPacket;
|
import net.minecraft.network.play.server.SUpdateLightPacket;
|
||||||
import net.minecraft.tileentity.TileEntity;
|
|
||||||
import net.minecraft.util.ClassInheritanceMultiMap;
|
|
||||||
import net.minecraft.world.chunk.Chunk;
|
|
||||||
|
|
||||||
@Mixin(ClientPlayNetHandler.class)
|
@Mixin(ClientPlayNetHandler.class)
|
||||||
public class NetworkLightUpdateMixin {
|
public class NetworkLightUpdateMixin {
|
||||||
|
@ -34,8 +26,8 @@ public class NetworkLightUpdateMixin {
|
||||||
int chunkX = packet.getX();
|
int chunkX = packet.getX();
|
||||||
int chunkZ = packet.getZ();
|
int chunkZ = packet.getZ();
|
||||||
|
|
||||||
LightUpdater.getInstance()
|
LightUpdater.get(world)
|
||||||
.onLightPacket(world, chunkX, chunkZ);
|
.onLightPacket(chunkX, chunkZ);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import net.minecraft.util.math.vector.Vector3f;
|
||||||
|
|
||||||
public class ChestInstance<T extends TileEntity & IChestLid> extends TileEntityInstance<T> implements IDynamicInstance {
|
public class ChestInstance<T extends TileEntity & IChestLid> extends TileEntityInstance<T> implements IDynamicInstance {
|
||||||
|
|
||||||
|
private final MatrixTransformStack stack = new MatrixTransformStack();
|
||||||
private final OrientedData body;
|
private final OrientedData body;
|
||||||
private final ModelData lid;
|
private final ModelData lid;
|
||||||
|
|
||||||
|
@ -91,9 +92,8 @@ public class ChestInstance<T extends TileEntity & IChestLid> extends TileEntityI
|
||||||
|
|
||||||
float angleX = -(progress * ((float) Math.PI / 2F));
|
float angleX = -(progress * ((float) Math.PI / 2F));
|
||||||
|
|
||||||
MatrixTransformStack stack = new MatrixTransformStack();
|
stack.setIdentity()
|
||||||
|
.translate(getInstancePosition())
|
||||||
stack.translate(getInstancePosition())
|
|
||||||
.translate(0, 9f/16f, 0)
|
.translate(0, 9f/16f, 0)
|
||||||
.centre()
|
.centre()
|
||||||
.multiply(baseRotation)
|
.multiply(baseRotation)
|
||||||
|
|
Loading…
Reference in a new issue