Created
August 22, 2015 01:56
-
-
Save TheGreyGhost/96983a0cd47c8c2f294d to your computer and use it in GitHub Desktop.
Rendering to a FrameBuffer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, where each column in that row is one of the six block faces | |
// 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment