# Minecraft贴图加载原理
**难度分级:☆☆☆☆☆☆☆**
这一章我们将深入了解Minecraft贴图的加载原理,初步探索OpenGL的纹理系统,并编写一个纹理动态加载器来**从外部读取贴图**。
**警告:在开始阅读之前请务必确保你已经基本掌握了基础篇8和进阶篇1的内容。**
## `bindTexture()`的原理和纹理加载流程 难度分级:☆☆☆☆☆☆
我们先看一下原版的`bindTexture()`是怎么实现的:
~~~
public void bindTexture(ResourceLocation resource)
{
ITextureObject itextureobject = this.mapTextureObjects.get(resource);
if (itextureobject == null)
{
itextureobject = new SimpleTexture(resource);
this.loadTexture(resource, itextureobject);
}
TextureUtil.bindTexture(itextureobject.getGlTextureId()); // 这个方法直接调用了GLStateManager的bindTexture()方法
}
~~~
这是`GLStateManager`的`bindTexture()`方法:
~~~
public static void bindTexture(int texture)
{
if (texture != textureState[activeTextureUnit].textureName)
{
textureState[activeTextureUnit].textureName = texture;
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture);
}
}
~~~
很明显,它只是获取了一个**TextureID**,**判断当前绑定的是不是这个贴图**,如果不是就调用**OpenGL的底层API**绑定贴图。
那么**TextureID**又是从哪来的呢?怎么**把实际的贴图与一个TextureID相关联**?
让我们看一下`ITextureObject`的一个实现类`SimpleTexture`的源码(已除去不必要的代码):
~~~
@SideOnly(Side.CLIENT)
public class SimpleTexture extends AbstractTexture
{
protected final ResourceLocation textureLocation;
public SimpleTexture(ResourceLocation textureResourceLocation)
{
this.textureLocation = textureResourceLocation;
}
public void loadTexture(IResourceManager resourceManager) throws IOException
{
this.deleteGlTexture();
IResource iresource = resourceManager.getResource(this.textureLocation);
BufferedImage bufferedimage = TextureUtil.readBufferedImage(iresource.getInputStream());
TextureUtil.uploadTextureImageAllocate(this.getGlTextureId(), bufferedimage, false, false);
}
}
~~~
继续往下查看,我们可以发现`glTextureId`是通过调用`GL11.glGenTextures()`获取的。
`TextureUtil.uploadTextureImageAllocate()`方法牵扯到Mipmap等硬核内容,本章不讲述。
此时,我们已经基本搞懂了纹理的加载流程:
1. 从一个`InputStream`里**读取一个`BufferedImage`**
2. **获取一个`TextureId`**
3. **将`BufferedImage`和`TextureId`关联起来,写入到纹理缓冲区里**
## 编写纹理动态加载器 难度分级:☆☆☆☆☆
在了解了纹理加载流程后,编写一个纹理动态加载器应该没有那么难了,只需要调用Minecraft上层API即可。
我们先编写一个实现了`ITextureObject`的类,因为`AbstractTexture`类已经帮我们接管大部分底层操作,我们直接继承它就行。
~~~
private static final class ExternalImageTexture extends AbstractTexture {
private BufferedImage image;
public ExternalImageTexture(File file) throws FileNotFoundException, IOException {
this.image = TextureUtil.readBufferedImage(new FileInputStream(file));
}
public ExternalImageTexture(BufferedImage image) {
this.image = image;
}
@Override
public void loadTexture(IResourceManager resourceManager) throws IOException {
this.deleteGlTexture();
TextureUtil.uploadTextureImageAllocate(this.getGlTextureId(), image, false, false);
}
}
~~~
以及`bindTexture()`方法:
~~~
public static void bindTexture(File file) throws IOException {
ITextureObject texture = new ExternalImageTexture(file);
texture.loadTexture(Minecraft.getMinecraft().getResourceManager());
TextureUtil.bindTexture(itextureobject.getGlTextureId());
}
public static void bindTexture(BufferedImage image) throws IOException {
ITextureObject texture = new ExternalImageTexture(image);
texture.loadTexture(Minecraft.getMinecraft().getResourceManager());
TextureUtil.bindTexture(itextureobject.getGlTextureId());
}
~~~
看起来没有什么问题,但是当我们打开GUI**绘制加载的这些贴图**的时候,我们会感到游戏变得**极度卡顿,游戏内存占用也在飞速上涨**。并且**关掉GUI后内存也没有被回收**。
很明显,我们引发了一个**持续内存泄漏问题**。
我们重新阅读之前写的代码,会发现我们在**每一帧被绘制的时候**都调用了**bindTexture()**方法,这在原版确实没有什么问题,但我们再次看一下原版的`bindTexture()`方法,里面有这么一段代码:
~~~
ITextureObject itextureobject = this.mapTextureObjects.get(resource);
if (itextureobject == null)
{
itextureobject = new SimpleTexture(resource);
this.loadTexture(resource, itextureobject);
}
~~~
从这里我们可以看到,原版代码里获取的`ITextureObject`是**已有的,唯一的,可复用的**,而我们刚刚编写的代码在每次`bindTexture()`的时候都**重新构造了一个`ITextureObject`,并重新分配了一个`glTextureId`**,而OpenGL加载的纹理是被默认为**会被再次使用的纹理**,所以它**不会被回收**。
那么我们只需要让我们动态加载的纹理**只会被分配一个**`glTextureId`,并**在GUI被关闭的时候删除贴图**就好了。
我们修改一下刚才的`bindTexture()`方法:
~~~
public static void bindTexture(int textureId) {
GlStateManager.bindTexture(textureId);
}
public static int loadTexture(File file) throws IOException {
ITextureObject texture = new ExternalImageTexture(file);
texture.loadTexture(Minecraft.getMinecraft().getResourceManager());
return texture.getGlTextureId();
}
public static int loadTexture(BufferedImage image) throws IOException {
ITextureObject texture = new ExternalImageTexture(image);
texture.loadTexture(Minecraft.getMinecraft().getResourceManager());
return texture.getGlTextureId();
}
public static void deleteTexture(int textureId) {
TextureUtil.deleteTexture(textureId);
}
~~~
我们在GUI被初始化的时候将所有外部纹理加载出来,并获取它的`glTextureId`:
~~~
glTextureId = GLUtils.loadTexture(this.image); // 我把刚才的那些方法写到了GLUtils类中
~~~
然后我们绑定纹理的时候仅绑定`textureId`:
~~~
GLUtils.bindTexture(glTextureId);
~~~
关闭GUI时删除贴图:
~~~
@Override
public void onGuiClosed() {
GLUtils.deleteTexture(glTextureId);
}
~~~
此时我们打开游戏,开关GUI后,可以发现内存占用不再持续上涨,并且内存也会被及时回收。
本章完。
- 0.引子
- 基础篇 - 1.构建开发环境
- 基础篇 - 2.主类和代理
- 基础篇 - 3.创建一个物品
- 基础篇 - 4.创建一个方块
- 基础篇 - 4.1自定义方块模型
- 基础篇 - 5.初探事件系统
- 基础篇 - 6.Capability系统
- 基础篇 - 7.创建一个方块实体
- 基础篇 - 8.你的第一个GUI
- 基础篇 - 9.网络与通讯
- 进阶篇 - 0.更复杂的Mod
- 进阶篇 - 1.Minecraft渲染原理
- 进阶篇 - 2.更复杂的GUI
- 进阶篇 - 3.物品进阶
- 高级篇 - 1.深入探索OpenGL
- 高级篇 - 1.1OpenGL颜色渲染
- 高级篇 - 1.2OpenGL光照系统
- 高级篇 - 2.Minecraft贴图加载原理