/* * This file is part of Applied Energistics 2. * Copyright (c) 2013 - 2018, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Applied Energistics 2 is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Applied Energistics 2. If not, see . */ package appeng.client.render.cablebus; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.function.Function; import javax.annotation.Nullable; import net.minecraft.block.BlockState; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.model.BakedModel; import net.minecraft.client.render.block.BlockRenderManager; import net.minecraft.client.renderer.RenderTypeLookup; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.render.model.BakedQuad; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.item.ItemStack; import net.minecraft.util.Identifier; import net.minecraft.util.math.Box; import net.minecraft.util.math.Direction; import net.minecraft.util.math.Direction.Axis; import net.minecraft.util.math.BlockPos; import net.minecraft.world.ILightReader; import net.minecraftforge.client.ForgeHooksClient; import net.minecraftforge.client.model.data.EmptyModelData; import appeng.api.AEApi; import appeng.api.util.AEAxisAlignedBB; import appeng.parts.misc.CableAnchorPart; import appeng.thirdparty.codechicken.lib.model.CachedFormat; import appeng.thirdparty.codechicken.lib.model.Quad; import appeng.thirdparty.codechicken.lib.model.pipeline.BakedPipeline; import appeng.thirdparty.codechicken.lib.model.pipeline.transformers.QuadAlphaOverride; import appeng.thirdparty.codechicken.lib.model.pipeline.transformers.QuadClamper; import appeng.thirdparty.codechicken.lib.model.pipeline.transformers.QuadCornerKicker; import appeng.thirdparty.codechicken.lib.model.pipeline.transformers.QuadFaceStripper; import appeng.thirdparty.codechicken.lib.model.pipeline.transformers.QuadReInterpolator; import appeng.thirdparty.codechicken.lib.model.pipeline.transformers.QuadTinter; /** * The FacadeBuilder builds for facades.. * * @author covers1624 */ public class FacadeBuilder { public static final double THICK_THICKNESS = 2D / 16D; public static final double THIN_THICKNESS = 1D / 16D; public static final Box[] THICK_FACADE_BOXES = new Box[] { new Box(0.0, 0.0, 0.0, 1.0, THICK_THICKNESS, 1.0), new Box(0.0, 1.0 - THICK_THICKNESS, 0.0, 1.0, 1.0, 1.0), new Box(0.0, 0.0, 0.0, 1.0, 1.0, THICK_THICKNESS), new Box(0.0, 0.0, 1.0 - THICK_THICKNESS, 1.0, 1.0, 1.0), new Box(0.0, 0.0, 0.0, THICK_THICKNESS, 1.0, 1.0), new Box(1.0 - THICK_THICKNESS, 0.0, 0.0, 1.0, 1.0, 1.0) }; public static final Box[] THIN_FACADE_BOXES = new Box[] { new Box(0.0, 0.0, 0.0, 1.0, THIN_THICKNESS, 1.0), new Box(0.0, 1.0 - THIN_THICKNESS, 0.0, 1.0, 1.0, 1.0), new Box(0.0, 0.0, 0.0, 1.0, 1.0, THIN_THICKNESS), new Box(0.0, 0.0, 1.0 - THIN_THICKNESS, 1.0, 1.0, 1.0), new Box(0.0, 0.0, 0.0, THIN_THICKNESS, 1.0, 1.0), new Box(1.0 - THIN_THICKNESS, 0.0, 0.0, 1.0, 1.0, 1.0) }; private final ThreadLocal pipelines = ThreadLocal.withInitial(() -> BakedPipeline.builder() // Clamper is responsible for clamping the vertex to the bounds specified. .addElement("clamper", QuadClamper.FACTORY) // Strips faces if they match a mask. .addElement("face_stripper", QuadFaceStripper.FACTORY) // Kicks the edge inner corners in, solves Z fighting .addElement("corner_kicker", QuadCornerKicker.FACTORY) // Re-Interpolates the UV's for the quad. .addElement("interp", QuadReInterpolator.FACTORY) // Tints the quad if we need it to. Disabled by default. .addElement("tinter", QuadTinter.FACTORY, false) // Overrides the quad's alpha if we are forcing transparent facades. .addElement("transparent", QuadAlphaOverride.FACTORY, false, e -> e.setAlphaOverride(0x4C / 255F)).build()// ); private final ThreadLocal collectors = ThreadLocal.withInitial(Quad::new); public void buildFacadeQuads(RenderLayer layer, CableBusRenderState renderState, Random rand, List quads, Function modelLookup) { BakedPipeline pipeline = this.pipelines.get(); Quad collectorQuad = this.collectors.get(); boolean transparent = AEApi.instance().partHelper().getCableRenderMode().transparentFacades; Map facadeStates = renderState.getFacades(); List partBoxes = renderState.getBoundingBoxes(); Set sidesWithParts = renderState.getAttachments().keySet(); ILightReader parentWorld = renderState.getWorld(); BlockPos pos = renderState.getPos(); BlockColors blockColors = MinecraftClient.getInstance().getBlockColors(); boolean thinFacades = isUseThinFacades(partBoxes); for (Entry entry : facadeStates.entrySet()) { Direction side = entry.getKey(); int sideIndex = side.ordinal(); FacadeRenderState facadeRenderState = entry.getValue(); boolean renderStilt = !sidesWithParts.contains(side); if (layer == RenderLayer.getCutout() && renderStilt) { for (Identifier part : CableAnchorPart.FACADE_MODELS.getModels()) { BakedModel partModel = modelLookup.apply(part); QuadRotator rotator = new QuadRotator(); quads.addAll(rotator.rotateQuads(gatherQuads(partModel, null, rand, EmptyModelData.INSTANCE), side, Direction.UP)); } } // If we are forcing transparency and this isn't the Translucent layer. if (transparent && layer != RenderLayer.getTranslucent()) { continue; } BlockState blockState = facadeRenderState.getSourceBlock(); // If we aren't forcing transparency let the block decide if it should render. if (!transparent && layer != null) { if (!RenderTypeLookup.canRenderInLayer(blockState, layer)) { continue; } } Box fullBounds = thinFacades ? THIN_FACADE_BOXES[sideIndex] : THICK_FACADE_BOXES[sideIndex]; Box facadeBox = fullBounds; // If we are a transparent facade, we need to modify out BB. if (facadeRenderState.isTransparent()) { double offset = thinFacades ? THIN_THICKNESS : THICK_THICKNESS; AEAxisAlignedBB tmpBB = null; for (Direction face : Direction.values()) { // Only faces that aren't on our axis if (face.getAxis() != side.getAxis()) { FacadeRenderState otherState = facadeStates.get(face); if (otherState != null && !otherState.isTransparent()) { if (tmpBB == null) { tmpBB = AEAxisAlignedBB.fromBounds(facadeBox); } switch (face) { case DOWN: tmpBB.minY += offset; break; case UP: tmpBB.maxY -= offset; break; case NORTH: tmpBB.minZ += offset; break; case SOUTH: tmpBB.maxZ -= offset; break; case WEST: tmpBB.minX += offset; break; case EAST: tmpBB.maxX -= offset; break; default: throw new RuntimeException("Switch falloff. " + String.valueOf(face)); } } } } if (tmpBB != null) { facadeBox = tmpBB.getBoundingBox(); } } AEAxisAlignedBB cutOutBox = getCutOutBox(facadeBox, partBoxes); List holeStrips = getBoxes(facadeBox, cutOutBox, side.getAxis()); ILightReader facadeAccess = new FacadeBlockAccess(parentWorld, pos, side, blockState); BlockRenderManager dispatcher = MinecraftClient.getInstance().getBlockRenderManager(); BakedModel model = dispatcher.getModelForState(blockState); IModelData modelData = model.getModelData(facadeAccess, pos, blockState, EmptyModelData.INSTANCE); List modelQuads = new ArrayList<>(); // If we are forcing transparent facades, fake the render layer, and grab all // quads. if (transparent || layer == null) { for (RenderLayer forcedLayer : RenderLayer.getBlockRenderTypes()) { // Check if the block renders on the layer we want to force. if (RenderTypeLookup.canRenderInLayer(blockState, forcedLayer)) { // Force the layer and gather quads. ForgeHooksClient.setRenderLayer(forcedLayer); modelQuads.addAll(gatherQuads(model, blockState, rand, modelData)); } } // Reset. ForgeHooksClient.setRenderLayer(layer); } else { modelQuads.addAll(gatherQuads(model, blockState, rand, modelData)); } // No quads.. Cool, next! if (modelQuads.isEmpty()) { continue; } // Grab out pipeline elements. QuadClamper clamper = pipeline.getElement("clamper", QuadClamper.class); QuadFaceStripper edgeStripper = pipeline.getElement("face_stripper", QuadFaceStripper.class); QuadTinter tinter = pipeline.getElement("tinter", QuadTinter.class); QuadCornerKicker kicker = pipeline.getElement("corner_kicker", QuadCornerKicker.class); // Set global element states. // calculate the side mask. int facadeMask = 0; for (Entry ent : facadeStates.entrySet()) { Direction s = ent.getKey(); if (s.getAxis() != side.getAxis()) { FacadeRenderState otherState = ent.getValue(); if (!otherState.isTransparent()) { facadeMask |= 1 << s.ordinal(); } } } // Setup the edge stripper. edgeStripper.setBounds(fullBounds); edgeStripper.setMask(facadeMask); // Setup the kicker. kicker.setSide(sideIndex); kicker.setFacadeMask(facadeMask); kicker.setBox(fullBounds); kicker.setThickness(thinFacades ? THIN_THICKNESS : THICK_THICKNESS); for (BakedQuad quad : modelQuads) { // lookup the format in CachedFormat. CachedFormat format = CachedFormat.lookup(DefaultVertexFormats.BLOCK); // If this quad has a tint index, setup the tinter. if (quad.hasTintIndex()) { tinter.setTint(blockColors.getColor(blockState, facadeAccess, pos, quad.getColorIndex())); } for (Box box : holeStrips) { // setup the clamper for this box clamper.setClampBounds(box); // Reset the pipeline, clears all enabled/disabled states. pipeline.reset(format); // Reset out collector. collectorQuad.reset(format); // Enable / disable the optional elements pipeline.setElementState("tinter", quad.hasTintIndex()); pipeline.setElementState("transparent", transparent); // Prepare the pipeline for a quad. pipeline.prepare(collectorQuad); // Pipe our quad into the pipeline. quad.pipe(pipeline); // Check if the collector got any data. if (collectorQuad.full) { // Add the result. quads.add(collectorQuad.bake()); } } } } } /** * This is slow, so should be cached. * * @return The model. */ public List buildFacadeItemQuads(ItemStack textureItem, Direction side) { List facadeQuads = new ArrayList<>(); BakedModel model = MinecraftClient.getInstance().getItemRenderer().getItemModelWithOverrides(textureItem, null, null); List modelQuads = gatherQuads(model, null, new Random(), EmptyModelData.INSTANCE); BakedPipeline pipeline = this.pipelines.get(); Quad collectorQuad = this.collectors.get(); // Grab pipeline elements. QuadClamper clamper = pipeline.getElement("clamper", QuadClamper.class); QuadTinter tinter = pipeline.getElement("tinter", QuadTinter.class); for (BakedQuad quad : modelQuads) { // Lookup the CachedFormat for this quads format. CachedFormat format = CachedFormat.lookup(DefaultVertexFormats.BLOCK); // Reset the pipeline. pipeline.reset(format); // Reset the collector. collectorQuad.reset(format); // If we have a tint index, setup the tinter and enable it. if (quad.hasTintIndex()) { tinter.setTint(MinecraftClient.getInstance().getItemColors().getColor(textureItem, quad.getColorIndex())); pipeline.enableElement("tinter"); } // Disable elements we don't need for items. pipeline.disableElement("face_stripper"); pipeline.disableElement("corner_kicker"); // Setup the clamper clamper.setClampBounds(THICK_FACADE_BOXES[side.ordinal()]); // Prepare the pipeline. pipeline.prepare(collectorQuad); // Pipe our quad into the pipeline. quad.pipe(pipeline); // Check the collector for data and add the quad if there was. if (collectorQuad.full) { facadeQuads.add(collectorQuad.bake()); } } return facadeQuads; } // Helper to gather all quads from a model into a list. private static List gatherQuads(BakedModel model, BlockState state, Random rand, IModelData data) { List modelQuads = new ArrayList<>(); for (Direction face : Direction.values()) { modelQuads.addAll(model.getQuads(state, face, rand, data)); } modelQuads.addAll(model.getQuads(state, null, rand, data)); return modelQuads; } /** * Given the actual facade bounding box, and the bounding boxes of all parts, * determine the biggest union of AABB that intersect with the facade's bounding * box. This AABB will need to be "cut out" when the facade is rendered. */ @Nullable private static AEAxisAlignedBB getCutOutBox(Box facadeBox, List partBoxes) { AEAxisAlignedBB b = null; for (Box bb : partBoxes) { if (bb.intersects(facadeBox)) { if (b == null) { b = AEAxisAlignedBB.fromBounds(bb); } else { b.maxX = Math.max(b.maxX, bb.maxX); b.maxY = Math.max(b.maxY, bb.maxY); b.maxZ = Math.max(b.maxZ, bb.maxZ); b.minX = Math.min(b.minX, bb.minX); b.minY = Math.min(b.minY, bb.minY); b.minZ = Math.min(b.minZ, bb.minZ); } } } return b; } /** * Generates the box segments around the specified hole. If the specified hole * is null, a Singleton of the Facade box is returned. * * @param fb The Facade's box. * @param hole The hole to 'cut'. * @param axis The axis the facade is on. * * @return The box segments. */ private static List getBoxes(Box fb, AEAxisAlignedBB hole, Axis axis) { if (hole == null) { return Collections.singletonList(fb); } List boxes = new ArrayList<>(); switch (axis) { case Y: boxes.add(new Box(fb.minX, fb.minY, fb.minZ, hole.minX, fb.maxY, fb.maxZ)); boxes.add(new Box(hole.maxX, fb.minY, fb.minZ, fb.maxX, fb.maxY, fb.maxZ)); boxes.add(new Box(hole.minX, fb.minY, fb.minZ, hole.maxX, fb.maxY, hole.minZ)); boxes.add(new Box(hole.minX, fb.minY, hole.maxZ, hole.maxX, fb.maxY, fb.maxZ)); break; case Z: boxes.add(new Box(fb.minX, fb.minY, fb.minZ, fb.maxX, hole.minY, fb.maxZ)); boxes.add(new Box(fb.minX, hole.maxY, fb.minZ, fb.maxX, fb.maxY, fb.maxZ)); boxes.add(new Box(fb.minX, hole.minY, fb.minZ, hole.minX, hole.maxY, fb.maxZ)); boxes.add(new Box(hole.maxX, hole.minY, fb.minZ, fb.maxX, hole.maxY, fb.maxZ)); break; case X: boxes.add(new Box(fb.minX, fb.minY, fb.minZ, fb.maxX, hole.minY, fb.maxZ)); boxes.add(new Box(fb.minX, hole.maxY, fb.minZ, fb.maxX, fb.maxY, fb.maxZ)); boxes.add(new Box(fb.minX, hole.minY, fb.minZ, fb.maxX, hole.maxY, hole.minZ)); boxes.add(new Box(fb.minX, hole.minY, hole.maxZ, fb.maxX, hole.maxY, fb.maxZ)); break; default: // should never happen. throw new RuntimeException("switch falloff. " + String.valueOf(axis)); } return boxes; } /** * Determines if any of the part's bounding boxes intersects with the outside 2 * voxel wide layer. If so, we should use thinner facades (1 voxel deep). */ private static boolean isUseThinFacades(List partBoxes) { final double min = 2.0 / 16.0; final double max = 14.0 / 16.0; for (Box bb : partBoxes) { int o = 0; o += bb.maxX > max ? 1 : 0; o += bb.maxY > max ? 1 : 0; o += bb.maxZ > max ? 1 : 0; o += bb.minX < min ? 1 : 0; o += bb.minY < min ? 1 : 0; o += bb.minZ < min ? 1 : 0; if (o >= 2) { return true; } } return false; } }