package speedytools.clientside.selections;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.resources.model.IBakedModel;
import net.minecraft.client.shader.Framebuffer;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import speedytools.common.utilities.ErrorLog;
import java.awt.*;
import java.nio.IntBuffer;
import java.util.*;
import java.util.List;
/**
* Created by TheGreyGhost on 14/03/2015.
*
* Used to track the textures used for each cube in a selection.
* A limited number of block textures will be stored. Once full a generic block texture will be substituted instead
*
* Usage:
* (1) Create the SelectionBlockTextures to allocate the space and create a texture sheet
* (2) addBlock() for each Block you want included in the texture sheet - doesn't actually capture the block's
* texture yet.
* (2b) alternatively, if the autoAllocate() is set to true, calling getSBTIcon() on an unused block will automatically
* allocate a space for that block
* (3) updateTextures() to capture the textures of all blocks you added using addBlock()
* When rendering:
* (4) bindTexture() to bind to the SelectionBlockTextures for subsequent rendering
* (5) getSBTIcon() to get the Icon (texture coordinates) for the given block
* Other tools:
* (6) clear() to empty the list of blocks on the sheet
* (7) release() to release the OpenGL texture sheet
*
* If you are creating and releasing a lot of SelectionBlockTextures you should release() to avoid using up
* the OpenGL memory.
*
*/
public class SelectionBlockTextures {
public SelectionBlockTextures(TextureManager i_textureManager) {
textureManager = i_textureManager;
int textureHeightTexels = BLOCK_COUNT_DESIRED * V_TEXELS_PER_FACE;
int maximumTextureHeight = Minecraft.getGLMaximumTextureSize();
if (textureHeightTexels <= maximumTextureHeight) {
BLOCK_COUNT = BLOCK_COUNT_DESIRED;
} else {
BLOCK_COUNT = maximumTextureHeight / V_TEXELS_PER_FACE;
textureHeightTexels = maximumTextureHeight;
}
LAST_BLOCK_INDEX_PLUS_ONE = FIRST_BLOCK_INDEX + BLOCK_COUNT - 1;
int textureWidthTexels = NUMBER_OF_FACES_PER_BLOCK * U_TEXELS_PER_FACE;
TEXEL_HEIGHT_PER_BLOCK = 1.0 / BLOCK_COUNT;
TEXEL_WIDTH_PER_FACE = 1.0 / NUMBER_OF_FACES_PER_BLOCK;
nextFreeTextureIndex = FIRST_BLOCK_INDEX;
firstUncachedIndex = FIRST_BLOCK_INDEX;
blockTextures = new DynamicTexture(textureWidthTexels, textureHeightTexels);
textureAllocated = true;
textureResourceLocation = textureManager.getDynamicTextureLocation("SelectionBlockTextures", blockTextures);
eraseBlockTextures(NULL_BLOCK_INDEX, NULL_BLOCK_INDEX);
eraseBlockTextures(FIRST_BLOCK_INDEX, LAST_BLOCK_INDEX_PLUS_ONE - 1);
blockTextures.updateDynamicTexture();
++sbtCount;
if (sbtCount > SBT_COUNT_WARNING) {
System.err.println("Warning: allocated " + sbtCount + " textures without release()");
}
}
/**
* If true, a call to getSBTIcon for a block with no allocated Icon will automatically allocate an Icon
* The default is false.
* @param autoAllocateIcon
*/
public void setAutoAllocateIcon(boolean autoAllocateIcon) {
this.autoAllocateIcon = autoAllocateIcon;
}
/** bind the texture sheet of the SelectionBlockTextures, ready for rendering the SBTicons
*/
public void bindTexture() {
textureManager.bindTexture(textureResourceLocation);
}
/** remove all blocks from the texture sheet
*/
public void clear()
{
blockTextureNumbers.clear();
nextFreeTextureIndex = FIRST_BLOCK_INDEX;
firstUncachedIndex = FIRST_BLOCK_INDEX;
}
/**
* release the allocated OpenGL texture
* do not use this object again after release
*/
public void release()
{
blockTextures.deleteGlTexture();
if (textureAllocated) {
--sbtCount;
assert sbtCount >= 0;
textureAllocated = false;
}
}
/**
* Include this block in the texture sheet
* (Doesn't actually stitch the block's texture into the sheet until you call updateTextures() )
* @param iBlockState the block to add. Ignores properties (uses block.getDefaultState())
*/
public void addBlock(IBlockState iBlockState)
{
if (iBlockState == null) {
return;
}
if (blockTextureNumbers.containsKey(iBlockState.getBlock())) {
return;
}
if (nextFreeTextureIndex >= LAST_BLOCK_INDEX_PLUS_ONE) {
return;
}
blockTextureNumbers.put(iBlockState.getBlock(), nextFreeTextureIndex);
++nextFreeTextureIndex;
}
/**
* Will stitch any newly-added blocks into the texture sheet
*/
public void updateTextures()
{
if (firstUncachedIndex >= nextFreeTextureIndex) return;
if (!OpenGlHelper.isFramebufferEnabled()) { // frame buffer not available, just use blank texture
eraseBlockTextures(firstUncachedIndex, nextFreeTextureIndex - 1);
firstUncachedIndex = nextFreeTextureIndex;
return;
}
Framebuffer frameBuffer = null;
try {
GL11.glPushAttrib(GL11.GL_ENABLE_BIT);
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glPushMatrix();
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glPushMatrix();
// setup modelview matrix
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glLoadIdentity();
GL11.glOrtho(0.0D, 1.0, 1.0, 0.0, -10.0, 10.0); // set up to render over [0,0,0] to [1,1,1]
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_DEPTH_TEST);
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glDisable(GL11.GL_BLEND);
final float ALPHA_TEST_THRESHOLD = 0.1F;
GL11.glAlphaFunc(GL11.GL_GREATER, ALPHA_TEST_THRESHOLD);
final boolean USE_DEPTH = true;
frameBuffer = new Framebuffer(U_TEXELS_PER_FACE, V_TEXELS_PER_FACE, USE_DEPTH);
Minecraft mc = Minecraft.getMinecraft();
BlockRendererDispatcher blockRendererDispatcher = mc.getBlockRendererDispatcher();
BlockModelShapes blockModelShapes = blockRendererDispatcher.getBlockModelShapes();
for (Map.Entry<Block, Integer> entry : blockTextureNumbers.entrySet()) {
int textureIndex = entry.getValue();
if (textureIndex >= firstUncachedIndex) {
Block blockToRender = entry.getKey();
IBlockState defaultState = blockToRender.getDefaultState();
IBakedModel ibakedmodel = blockModelShapes.getModelForState(defaultState);
if (ibakedmodel instanceof net.minecraftforge.client.model.ISmartBlockModel) {
ibakedmodel = ((net.minecraftforge.client.model.ISmartBlockModel)ibakedmodel).handleBlockState(defaultState);
}
final int BAKED_MODEL_RENDER_TYPE = 3;
if (blockToRender.getRenderType() != BAKED_MODEL_RENDER_TYPE) {
eraseBlockTextures(textureIndex, textureIndex);
} else {
stitchModelIntoTextureSheet(frameBuffer, textureIndex, blockToRender, ibakedmodel);
}
}
}
} catch (Exception e) {
ErrorLog.defaultLog().info(e.toString());
} finally {
if (frameBuffer != null) {
frameBuffer.deleteFramebuffer();
}
GL11.glMatrixMode(GL11.GL_PROJECTION);
GL11.glPopMatrix();
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glPopMatrix();
GL11.glPopAttrib();
}
firstUncachedIndex = nextFreeTextureIndex;
blockTextures.updateDynamicTexture();
}
public void stitchModelIntoTextureSheet(Framebuffer frameBuffer, int textureIndex, Block blockToRender, IBakedModel iBakedModel) {
// from RenderFallingBlock.doRender()
switch (blockToRender.getBlockLayer()) {
case SOLID: {
GL11.glDisable(GL11.GL_ALPHA_TEST);
break;
}
case CUTOUT_MIPPED:
case CUTOUT:
case TRANSLUCENT: {
GL11.glEnable(GL11.GL_ALPHA_TEST);
break;
}
default: {
System.err.println("Unknown getBlockLayer():" + blockToRender.getBlockLayer());
return;
}
}
List<BakedQuad> generalQuads = iBakedModel.getGeneralQuads();
for (EnumFacing facing : EnumFacing.values()) {
List<BakedQuad> faceQuads = iBakedModel.getFaceQuads(facing);
final float BASE_COLOUR = 0.0F;
frameBuffer.setFramebufferColor(BASE_COLOUR, BASE_COLOUR, BASE_COLOUR, BASE_COLOUR);
frameBuffer.framebufferClear();
final boolean SET_VIEWPORT_TRUE = true;
frameBuffer.bindFramebuffer(SET_VIEWPORT_TRUE);
try {
// various transforms are required to make the world faces render appropriately.
// in some cases the face must be mirror imaged and the front face swapped with the back face (CW instead of CCW)
GL11.glPushMatrix();
GL11.glTranslatef(+0.5F, +0.5F, +0.5F);
GL11.glFrontFace(GL11.GL_CCW);
switch (facing) {
case NORTH:
GL11.glScalef(-1.0F, 1.0F, 1.0F);
GL11.glRotatef(0.0F, 0.0F, 1.0F, 0.0F);
GL11.glFrontFace(GL11.GL_CW);
break;
case SOUTH:
GL11.glScalef(-1.0F, 1.0F, 1.0F);
GL11.glRotatef(180.0F, 0.0F, 1.0F, 0.0F);
GL11.glFrontFace(GL11.GL_CW);
break;
case EAST:
GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F);
break;
case WEST:
GL11.glScalef(-1.0F, 1.0F, 1.0F);
GL11.glRotatef(270.0F, 0.0F, 1.0F, 0.0F);
GL11.glFrontFace(GL11.GL_CW);
break;
case UP:
GL11.glRotatef(-90.0F, 1.0F, 0.0F, 0.0F);
break;
case DOWN:
GL11.glRotatef(90.0F, 1.0F, 0.0F, 0.0F);
GL11.glRotatef(180.0F, 0.0F, 1.0F, 0.0F);
break;
}
GL11.glTranslatef(-0.5F, -0.5F, -0.5F);
Tessellator tessellator = Tessellator.getInstance();
WorldRenderer worldrenderer = tessellator.getWorldRenderer();
worldrenderer.startDrawingQuads();
worldrenderer.setVertexFormat(DefaultVertexFormats.BLOCK);
Minecraft.getMinecraft().getTextureManager().bindTexture(TextureMap.locationBlocksTexture);
if (!faceQuads.isEmpty()) {
renderModelStandardQuads(worldrenderer, faceQuads);
}
if (!generalQuads.isEmpty()) {
renderModelStandardQuads(worldrenderer, generalQuads);
}
tessellator.draw();
stitchGreyFrameBufferIntoTextureSheet(frameBuffer, textureIndex, facing);
} finally {
GL11.glPopMatrix();
}
}
}
/**
* stitch the given framebuffer into the texture sheet at the appropriate location.
* converts the framebuffer from colour to greyscale
* @param frameBuffer
* @param textureIndex
* @param whichFace
*/
private void stitchGreyFrameBufferIntoTextureSheet(Framebuffer frameBuffer, int textureIndex, EnumFacing whichFace)
{
frameBuffer.bindFramebufferTexture();
IntBuffer pixelBuffer = BufferUtils.createIntBuffer(U_TEXELS_PER_FACE * V_TEXELS_PER_FACE);
GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, pixelBuffer);
int[] frameData = new int[pixelBuffer.remaining()];
pixelBuffer.get(frameData);
int[] textureSheet = blockTextures.getTextureData();
int textureWidthTexels = NUMBER_OF_FACES_PER_BLOCK * U_TEXELS_PER_FACE;
int textureSheetBase = textureIndex * V_TEXELS_PER_FACE * textureWidthTexels
+ whichFace.getIndex() * U_TEXELS_PER_FACE;
for (int v = 0; v < V_TEXELS_PER_FACE; ++v) {
for (int u = 0; u < U_TEXELS_PER_FACE; ++u) {
int sourceColour = frameData[u + v * U_TEXELS_PER_FACE];
int red = sourceColour & 0xff;
int green = (sourceColour & 0xff00) >> 8;
int blue = (sourceColour & 0xff0000) >> 16;
int average = (red + blue + green) / 3;
int grey = average | (average << 8) | (average << 16) | 0xff000000;
textureSheet[textureSheetBase + u + v * textureWidthTexels] = grey;
}
}
}
/**
* render the given quad list at full brightness
* @param worldRendererIn
* @param bakedQuadList
*/
private void renderModelStandardQuads(WorldRenderer worldRendererIn, List<BakedQuad> bakedQuadList)
{
final int VERTEX_BRIGHTNESS = -1;
for (BakedQuad bakedQuad : bakedQuadList ) {
worldRendererIn.addVertexData(bakedQuad.getVertexData());
worldRendererIn.putBrightness4(VERTEX_BRIGHTNESS, VERTEX_BRIGHTNESS, VERTEX_BRIGHTNESS, VERTEX_BRIGHTNESS);
}
}
/**
* get the icon (position in the texture sheet) of the given block
* If autoAllocatedIcon() has been set, a call to getSBTIcon for a block with no allocated Icon will
* automatically allocate an Icon, otherwise a generic null icon is returned instead.
* The default is false, i.e. do not automatically allocate.
* @param iBlockState block to be retrieved (properties ignored)
* @param whichFace which faces' texture?
* @return the blocks' icon, or a generic "null" icon if not available
*/
public SBTIcon getSBTIcon(IBlockState iBlockState, EnumFacing whichFace)
{
Integer textureIndex;
if (iBlockState == null) {
textureIndex = NULL_BLOCK_INDEX;
} else {
if (!blockTextureNumbers.containsKey(iBlockState.getBlock())
&& autoAllocateIcon) {
addBlock(iBlockState);
}
if (!blockTextureNumbers.containsKey(iBlockState.getBlock())) {
textureIndex = NULL_BLOCK_INDEX;
} else {
textureIndex = blockTextureNumbers.get(iBlockState.getBlock());
if (textureIndex == null) {
textureIndex = NULL_BLOCK_INDEX;
}
}
}
double umin = TEXEL_WIDTH_PER_FACE * whichFace.getIndex();
double vmin = TEXEL_HEIGHT_PER_BLOCK * textureIndex;
return new SBTIcon(umin, umin + TEXEL_WIDTH_PER_FACE, vmin, vmin + TEXEL_HEIGHT_PER_BLOCK);
}
/**
* Overwrite the given texture indices with blank (untextured white)
* Must call updateDynamicTexture() afterwards to upload
* @param firstTextureIndex first texture index to blank out
* @param lastTextureIndex last texture index to blank out (inclusive)
*/
private void eraseBlockTextures(int firstTextureIndex, int lastTextureIndex)
{
int[] rawTexture = blockTextures.getTextureData();
int textureWidthTexels = NUMBER_OF_FACES_PER_BLOCK * U_TEXELS_PER_FACE;
for (int i = firstTextureIndex; i <= lastTextureIndex; ++i) {
int startRawDataIndex = textureWidthTexels * V_TEXELS_PER_FACE * firstTextureIndex;
int endRawDataIndexPlus1 = textureWidthTexels * V_TEXELS_PER_FACE * (lastTextureIndex + 1);
Arrays.fill(rawTexture, startRawDataIndex, endRawDataIndexPlus1, Color.WHITE.getRGB() );
}
}
// The face textures are stored as:
// one row per block, one column per face
// i.e. the texture sheet is six faces wide and BLOCK_COUNT faces high.
private final DynamicTexture blockTextures;
private final ResourceLocation textureResourceLocation;
private final TextureManager textureManager;
private final double TEXEL_WIDTH_PER_FACE;
private final double TEXEL_HEIGHT_PER_BLOCK;
private int nextFreeTextureIndex;
private int firstUncachedIndex;
private final int NUMBER_OF_FACES_PER_BLOCK = 6;
private final int U_TEXELS_PER_FACE = 16;
private final int V_TEXELS_PER_FACE = 16;
private final int NULL_BLOCK_INDEX = 0;
private final int FIRST_BLOCK_INDEX = NULL_BLOCK_INDEX + 1;
private final int BLOCK_COUNT_DESIRED = 256;
private final int BLOCK_COUNT;
private final int LAST_BLOCK_INDEX_PLUS_ONE;
private HashMap<Block, Integer> blockTextureNumbers = new HashMap<Block, Integer>(BLOCK_COUNT_DESIRED);
private boolean autoAllocateIcon = false;
public class SBTIcon
{
public SBTIcon(double i_umin, double i_umax, double i_vmin, double i_vmax)
{
umin = i_umin;
umax = i_umax;
vmin = i_vmin;
vmax = i_vmax;
}
public double getMinU() {
return umin;
}
public double getMaxU() {
return umax;
}
public double getMinV() {
return vmin;
}
public double getMaxV() {
return vmax;
}
private double umin;
private double umax;
private double vmin;
private double vmax;
}
// debug- to help detect resource leaks
private static final int SBT_COUNT_WARNING = 5; // if we have more than 5 opened-but-not-released objects, give a warning
private static int sbtCount = 0;
private boolean textureAllocated = false;
}