ShioZakana 发表于 2023-12-24 07:36:32

【Modding基础】"SpriteAPI"与"Fixed Rendering Pipeline"

本帖最后由 ShioZakana 于 2024-4-11 03:31 编辑

# 前言 该文章目的在于说明SpriteAPI部分方法的效果,以及其所对应的渲染流程。 由于只是基础内容,过程中将不会涉及复杂的代码等(包括OpenGL内容),仅讲解常用的部分。
 对于部分美术相关内容,该文章中不予讲解,如需请自行使用搜索引擎查找。 然而,考虑到其 性能与效率 ,完全 不建议 在自己的Mod制作过程中, 大规模 使用文中提及的方法绘制图形。 文章中的内容截至至文章发表日期,不代表完全适用于游戏本体往后的更新内容。


# SpriteAPI
(1) 简介
 这是在Modding过程中,原版环境下打包好提供给Modder使用的一个接口类:package com.fs.starfarer.api.graphics;
public interface SpriteAPI {} 在 Starsector 中,大部分图像显示相关的操作都是建立在 SpriteAPI 之上的,你可以从许多地方获得实现了该接口类的实例,包括但不限于Global.getSettings().getSprite() ship.getSpriteAPI() weapon.getSprite() 。
 类中的方法全部已被原版底层代码实现,你可以直接获取一个该类的实例,在Modding过程中使用,以便在屏幕上绘制所需图像。
 对于使用 Global.getSettings().getSprite() 获取实例对象,强烈建议在Mod文件中的 settings.json 内,提前为所需的本地图像文件进行注册,以确保能顺利获取到有效的实例对象。


(2) 参考坐标系
 为了便于理解,将为SpriteAPI对象建立一个 纹理空间 :
  
 在所建立的纹理空间中,绿色点 origin 为该实例的原点,对应的坐标设为 (0, 0) , width 与 height 为该实例设定的宽与高,而其蓝色点的中心位置 center 通常设定为 (width / 2, height / 2) 。而这些属性之中,除开原点外,你可以为每个实例单独设置一个所需的值。

 然而,单单有这些数据是并不能确定图像如何绘制到屏幕的,我们需要在纹理空间的基础上,建立另一个参考坐标系为每个坐标映射对应的颜色值,则接下来需要为该实例再引入一个 UV空间 的概念:
  
 同样的,在所建立的UV空间中,绿色点同样 origin 为原点,对应的坐标同样设为 (0, 0) 。
 然而,宽与高则与纹理空间不同, u 与 v 为该空间下设定的宽与高,并且他们是 归一化 的,即一个图像中,完整的u或者v长度均为 1.0 ,并且通常使用浮点数表示。
 在完整的 u 与 v 长度皆为 1.0 且原点 origin 对齐的情况下,将映射一个完整的贴图。
  
 另如图所示,当 u 与 v 取 0.5 ,且原点 origin 设为 (0.25, 0.25) 时,所截取得到的结果为,基于图像中心的二分之一的该图像。
 借助UV空间,我们可以确定实例在完成光栅化并渲染到屏幕上后,对应位置上的像素是我们想要的颜色值;至于获得颜色值所使用的采样方式则不多进行讲解,游戏中默认的采样方式为线性过滤。

(3) 尺寸与UV设定方法
 在SpriteAPI中,有如下方法获取或设定部分前文所述的值:// 中心
void setCenter(float x, float y);
void setCenterY(float cy);
void setCenterX(float cx);
float getCenterX();
float getCenterY();

// 尺寸
void setSize(float width, float height);
void setWidth(float width);
void setHeight(float height);
float getWidth();
float getHeight();

// UV
void setTexX(float texX); // UV原点X
void setTexY(float texY); // UV原点Y
void setTexWidth(float texWidth); // UV截取长度X
void setTexHeight(float texHeight); // UV截取长度Y
 那么,若uv坐标取值超过 后,是否仍然能够获取有效的信息?答案是肯定的。
  、
 如图所示,这是一个使用了 重复纹理图像 的环绕方式,橙色为贴图内容,绿色点仍为默认的原点,但坐标扩展至四个象限。
 无论坐标分量取何值,只要环绕方式设定正确,且是个能表示的有理数,都能获取到正确信息,无需担心程序出错;基于此,你甚至可以为每个分量取值 -128.001 或者 123.4567 ,但为了代码的可维护性与可读性,还是建议设定一个正常人能看得懂的值。
 在后文所介绍OpenGL中,这种环绕方式对应 GL_REPEAT ,也是游戏中默认的环绕方式。


(4) 内置的绘制方法
 每一个 SpriteAPI 对象都拥有一套可用的绘制方法,用于在调用时,根据设定好的参数在战场/生涯大地图/用户界面的某处绘制图形。
 由于方法名非常直观,大部分方法请配合示例图理解,不进行过多讲解。


  void render(float x, float y); 以 原点 为基础,对齐至所输入坐标值,绘制到指定位置。


  void renderAtCenter(float x, float y); 以 中点 为基础,对齐至所输入坐标值,绘制到指定位置。 若未调用 setCenter() setCenterY() setCenterY() 方法手动设定,会自行根据图片尺寸计算中心点。



  void renderRegion(float x, float y, float tx, float ty, float tw, float th); 以 原点 为基础,对齐至所输入坐标值,并根据uv空间的设定截取部分图像,在只显示截取部分的情况下绘制到指定位置。



  void renderRegionAtCenter(float x, float y, float tx, float ty, float tw, float th); 以 中点 为基础,对齐至所输入坐标值,并根据uv空间的设定截取部分图像,在只显示截取部分的情况下绘制到指定位置。‘ 若未调用 setCenter() setCenterY() setCenterY() 方法手动设定,会自行根据图片尺寸计算中心点。



  void renderWithCorners(float blX, float blY, float tlX, float tlY, float trX, float trY, float brX, float brY); 取四个二维位置点 {BL.xy, TL.xy, TR.xy, BR.xy} ,平均计算得出 中心点 ,使用 Triangle Fan 的形式依次绘制 6 次顶点,顺序为 中心点 => BL => TL => TR => BR => BL ;由于并未提供 offset 的设置方法,需要手动计算出偏移至目标位置后的点。

(5) 其余实例方法
 除开上文介绍外,还有另一些实例方法用于获取或设定绘制属性:float getAngle(); // 获取图像角度,通常取值
void setAngle(float angle); // 设定图像角度,通常取值 ,但对于WeaponAPI的图形无效

Color getColor(); // 获取颜色,三分量RGB颜色;正常情况不考虑Alpha通道
void setColor(Color color); // 设定颜色, 同上
Color getAverageColor(); // 纹理平均颜色,同上
Color getAverageBrightColor(); // 纹理平均亮度颜色,为灰度颜色,其余同上

float getAlphaMult(); // 获取透明度,通常取,默认值为1
void setAlphaMult(float alphaMult); // 设定透明度,但对于WeaponAPI的图形无效

void setBlendFunc(int src, int dest); // 设置混合方法
void setNormalBlend(); // 默认,通常为舰船或导弹的渲染
void setAdditiveBlend(); // 通常用于发光特效

int getTextureId(); // 获取该实例对应的OpenGL的纹理对象ID
void bindTexture(); // 在OpenGL渲染操作中绑定该实例对应的纹理对象

float getTextureWidth(); // 实际归一化UV映射坐标,其余见原版方法内注释
float getTextureHeight(); // 实际归一化UV映射坐标,其余见原版方法内注释 配合上这些方法,已经足够在原版环境下完成大多数绘制任务;而对于自定义绘制,则在下文开始介绍。



# Fixed Rendering Pipeline
(1) 简介
  Fixed Rendering Pipeline (固定渲染管线) 是 Starsector 游戏中大量(可以肯定是几乎全部,且包括文章发表时的大部分常见前置Mod)图形绘制使用的渲染流程,优点在于完全由 CPU端代码 控制,适用于简单但难以批量渲染,且对多种本地变量交互要求高的场合,或不熟悉GPU绘制的Modder使用,而缺点在于拓展性不高,大规模使用时性能低下(大量drawcall);然而这种渲染流程已在高版本的OpenGL开发中被废弃,取而代之的是另一种更现代化更高效的方式,即 Programmable Rendering Pipeline (可编程渲染管线) ,有时也被称为 Scriptable Rendering Pipeline 。
 截至文章发表时,游戏所使用的OpenGL库来源于 starsector-core 文件夹中包含的 LWJGL2.9.3 ,支持至 OpenGL4.5 版本。
 前文所提到的 SpriteAPI 实例,即为将该渲染流程相关代码包装好,提供给Modder使用的类。
 后文内容将介绍固定管线部分,而不对可编程管线进行讲解,也不对缓存对象进行探究;且由于其性质,大部分内容不便使用图片表示,请配合描述与代码进行理解。

 固定管线的大部分指令可在如下OpenGL相关类中找到:package org.lwjgl.opengl;
public final class GL11 {}
public final class GL12 {}
public final class GL13 {}
public final class GL14 {}
public final class GL15 {}

package org.lwjgl.util.glu;
public final class GLU {} 其中, GL11 其名称所含意思为 OpenGL 1.1 ,即其中特性于OpenGL的 1.1版本 时被支持, GLxx 等以此类推。 后文所提及的OpenGL相关方法除另行注明外,皆来自 GL11 。

 正常来说,使用OpenGL库渲染时,将遵循以下流程 创建OpenGL上下文/窗口 => 渲染循环 => 清理并关闭 ;游戏本体代码已经为你配置好了第一步与第三步,以及第二步的部分设定内容,你只需在Modding过程中按照实现顺序编写渲染循环内的相关代码即可。


(2) 矩阵操作
 在OpenGL中,大部分参数使用的都是 归一化 的量,包括游戏窗口中的坐标,通常使用多个 4×4矩阵 应用至绘制内容,将各种位置状态信息变换至所需坐标系中。
 在每个渲染循环中,原版代码已为你配置好了渲染所需的多个矩阵,这些矩阵计算后对应的裁剪空间可大致描述为: 以窗口上方向为正Y,右方向为正X,指向屏幕外方向为正Z的,尺寸为窗口长宽的正交投影视图;其中摄像机位于窗口中心,摄像机上轴为正Y,目视指向负Z 。

 为了便于绘制,为每一个图形应用模型矩阵会是一个好主意,GL11中提供如下方法分别应用对应的矩阵变换:// 平移变换
public static void glTranslatef(float x, float y, float z);
public static void glTranslated(double x, double y, double z);

// 旋转变换,但注意会引起 "Gimbal Lock (万向节死锁)"
// 根据 x/y/z 设定的向量为基准轴旋转
public static void glRotatef(float angle, float x, float y, float z);
public static void glRotated(double angle, double x, double y, double z);

// 缩放变换,使用倍率而不是目标尺寸
public static void glScalef(float x, float y, float z);
public static void glScaled(double x, double y, double z); 这些方法可以很轻松地完成模型矩阵设定,而不用理解其中的数学过程。 不过,需要注意这些变换应用时的顺序:如果我们需要将位置向正Y方向移动 5 个单位,并且将图形沿正Z轴旋转 45 度,则应该先执行平移再旋转;如果先进行旋转再平移,那么结果就会变成:图形的确是旋转了,但是移动方向则变成了朝 斜上方45度 移动了5个单位。
 然而,单单在代码内使用的话,这些操作会对 全局 的矩阵设定造成影响。为了确保该次绘制不影响其余所有操作,我们需要在绘制前保存当前矩阵,并在绘制完毕后应用回保存的矩阵,于是有如下 成对使用 的方法:public static void glPushMatrix(); // 将矩阵设置压入堆栈
public static void glPopMatrix(); // 恢复保存的矩阵设置 在每次绘制且需要进行矩阵操作的情况下,都应当使用这两行代码,将实际绘制代码 包围在其中 。

 当然,如果你了解矩阵操作,同样也可以使用其他方法设定矩阵:// GL11
public static void glLoadIdentity(); // 载入一个4×4单位矩阵,对角线为左上角(m00)与右下角(m33)
public static void glLoadMatrix(FloatBuffer m); // 载入一个4×4矩阵,各分量为float
public static void glLoadMatrix(DoubleBuffer m); // 载入一个4×4矩阵,各分量为double
public static void glMultMatrix(FloatBuffer m); // 将载入矩阵与当前矩阵乘算
public static void glMultMatrix(DoubleBuffer m); // 将载入矩阵与当前矩阵乘算
public static void glMatrixMode(int mode); // 用于设定特定定义的矩阵

// GL13
// 同GL11,但是载入时将进行转置操作
public static void glLoadTransposeMatrix(FloatBuffer m);
public static void glLoadTransposeMatrix(DoubleBuffer m);
public static void glMultTransposeMatrix(FloatBuffer m);
public static void glMultTransposeMatrix(DoubleBuffer m); 对于其中的 FloatBuffer 、 DoubleBuffer ,或者其余OpenGL方法中的 Buffer 类参数,你可以使用 org.lwjgl.BufferUtils 创建对应实例,并且写入完所有数据后不要忘记进行 flip() 操作。
 在 GLU 中同样有相关矩阵设定,但此处不多进行讲解。


(3) 混合操作
 在前文 SpriteAPI 中,有曾提到过以下方法:void setBlendFunc(int src, int dest);
void setNormalBlend();
void setAdditiveBlend(); 这些方法实际为OpenGL中的 混合 属性设定方法,用于定义绘制的图形(记为 源 )如何混合到当前画面或帧缓冲(记为 目标 )中,即源颜色与目标颜色通过如何的 数学运算 得到最终颜色;相关的基础OpenGL方法如下:// GL11
public static void glBlendFunc(int sfactor, int dfactor); // 设置 "源混合因子" 与 "目标混合因子"

// GL14
public static void glBlendFuncSeparate(int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha); // 同 GL11 ,但是对RGB与Alpha通道分别设置
public static void glBlendEquation(int mode); // 设置运算方法,默认情况下无需调用
// 可用的运算方法如下
GL14.GL_FUNC_ADD; // 默认值
GL14.GL_FUNC_SUBTRACT;
GL14.GL_FUNC_REVERSE_SUBTRACT; 其中,可用的因子如下:GL11.GL_ZERO;
GL11.GL_ONE;

GL11.GL_SRC_COLOR;
GL11.GL_ONE_MINUS_SRC_COLOR;
GL11.GL_SRC_ALPHA;
GL11.GL_ONE_MINUS_SRC_ALPHA;
            
GL11.GL_DST_COLOR;
GL11.GL_ONE_MINUS_DST_COLOR;
GL11.GL_DST_ALPHA;
GL11.GL_ONE_MINUS_DST_ALPHA;

GL11.GL_CONSTANT_COLOR;
GL11.GL_ONE_MINUS_CONSTANT_COLOR;
GL11.GL_CONSTANT_ALPHA;
GL11.GL_ONE_MINUS_CONSTANT_ALPHA;

GL11.GL_SRC_ALPHA_SATURATE;
 经过验证可知,SpriteAPI中两种预设混合方式的对应关系如下:
void setNormalBlend();
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

void setAdditiveBlend();
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE);
 前段文本提到过,混合是一个属性,所以我们需要在进行操作时,将混合选项打开;同样的,我们也可以在不需要混合时显式调用相关代码关闭:
public static void glEnable(int cap);
public static void glDisable(int cap);

// 对于混合属性,需填入的值如下
GL11.GL_BLEND; 除开进行混合外,对于OpenGL中的其他部分属性,我们都需要使用这两种方法填入对应属性类型进行操作。

 另外,类似于矩阵操作,属性也可以使用压入或释放堆栈的方式对进行存取:
public static void glPushAttrib(int mask);
public static void glPopAttrib();

// 对于混合选项或其他大多数使用 glEnable() 启用的属性,可以使用如下填入 mask
GL11.GL_ENABLE_BIT; 然而,原版与大多数Mod并没有使用这对方法进行状态管理,而是直接开启相关属性后进行渲染,但是并没有造成渲染错误,原因在于其属性并不影响渲染流程,并且几乎所有渲染都会调用一次;而对于一些游戏通常不会使用的属性例如 深度测试 与 面剔除 ,在不进行状态管理的情况下很容易造成 渲染错误 。
 OpenGL代码运行时本身就是一个巨大的状态机,因此各种状态管理是非常重要的。
 此外,该操作另拥有一对客户端方法,用于保存例如顶点缓存属性的启用设置等,但游戏与本文章中不太用得上,故不予讲解。

 实际编写代码时,你可能会注意到这些常量都是 int 类型的值,因为在OpenGL本体内(C语言,而非Java实现的Native方法),OpenGL的大多数设定值的都为 GLenum 类型,其属于一种符号常量。
 你当然可以直接使用int值填入其中,而不是使用获取字段的方法赋值,但是大多数场合下,为了你编写代码与别人阅览时不会看得头昏眼花,还请不要这么做。


(4) 基础绘制
 至此,对于状态配置的方法已大致了解完毕,本文章在接下来将试图实现绘制一个矩形,其伪代码如下:
pushMatrix();
applyModelMatrix();
setBlendFunc();
draw(); // 所需的绘制代码
popMatrix();
 同时,为了便于我们检测绘制情况与理解代码的工作效果,不妨额外设定一些目标:渲染一个红色半透明的矩形,该矩形附着于玩家旗舰,并且匹配舰船朝向与贴图尺寸。


 为了实现绘制内容的操作,一个十分常见的,大多数Mod都使用的绘制方法则是以下配合使用的代码对:
public static void glBegin(int mode);
public static void glEnd(); 这一对方法定义了我们的代码在何时开始执行一轮绘制,何时结束一轮绘制;他们一同管理了OpenGL中固定管线的绘制状态。
 对于其中开始绘制方法的参数,有如下可用项:
GL11.GL_POINTS;
            
GL11.GL_LINES;
GL11.GL_LINE_LOOP;
GL11.GL_LINE_STRIP;
            
GL11.GL_TRIANGLES;
GL11.GL_TRIANGLE_STRIP;
GL11.GL_TRIANGLE_FAN;
            
GL11.GL_QUADS;
GL11.GL_QUAD_STRIP;

GL11.GL_POLYGON;
 对于简单地绘制一个矩形,我们可以选择 GL_QUADS 填入参数,作为该次绘制的模式进行使用:
GL11.glBegin(GL11.GL_QUADS); 与前文提到的矩阵与属性状态存取一样,也必须成对使用。

 很明显,仅仅调用该方法的情况下,OpenGL是不知道我们到底要绘制一个怎样的矩形的,所以我们需要四个 顶点 来定义这个矩形。
 对此,OpenGL提供了如下方法用于设定各顶点坐标:
public static void glVertex2i(int x, int y);
public static void glVertex2f(float x, float y);
public static void glVertex2d(double x, double y);

public static void glVertex3i(int x, int y, int z);
public static void glVertex3f(float x, float y, float z);
public static void glVertex3d(double x, double y, double z);

public static void glVertex4i(int x, int y, int z, int w);
public static void glVertex4f(float x, float y, float z, float w);
public static void glVertex4d(double x, double y, double z, double w); 在游戏中,通常所有图像在视觉上都处于 同一个平面 (实际上大部分图形顶点的确是Z=0),所以我们只需要为每个坐标确定 x 与 y 即可;进一步地,由于游戏中大多数的数值都是 float 类型,最终我们确定将在接下来的绘制操作中使用 glVertex2f() 作为顶点设定方法。

 实际 GL11 中还拥有使用Buffer设定顶点数据的方法,但不予讲解,后同。

 由此,可以得出定义并绘制该矩形的部分代码如下:
// 指示开始绘制矩形
GL11.glBegin(GL11.GL_QUADS);
// 依次设定顶点
GL11.glVertex2f(-size.x, -size.y);
GL11.glVertex2f(-size.x, size.y);
GL11.glVertex2f(size.x, size.y);
GL11.glVertex2f(size.x, -size.y);
// 指示绘制结束
GL11.glEnd(); 其中,顶点设定的顺序是很重要的,顺序决定了每个顶点的连接顺序;若顺序错误,则会渲染出扭曲的图形。


 接下来,我们需要确定这个矩形的颜色与透明度。
 对于OpenGL的固定渲染管线,我们可以使用如下方法来进行设定:
public static void glColor3b(byte red, byte green, byte blue);
public static void glColor3ub(byte red, byte green, byte blue); // 实际为无符号byte,通常使用int显式转换
public static void glColor3f(float red, float green, float blue);
public static void glColor3d(double red, double green, double blue);

public static void glColor4b(byte red, byte green, byte blue, byte alpha);
public static void glColor4ub(byte red, byte green, byte blue, byte alpha); // 实际为无符号byte,通常使用int显式转换
public static void glColor4f(float red, float green, float blue, float alpha);
public static void glColor4d(double red, double green, double blue, double alpha); 为了顺应OpenGL的归一化数值,并且对设定透明度有需求,且迎合原版数据类型,此处选择 glColor4f() 作为设定方法。
 这一系列代码的作用是指示OpenGL接下来执行绘制指令时,每个顶点时所使用的颜色,也就是说,你完全可用在绘制途中重新指定,为每个顶点设定不同的颜色。
 另对于颜色设定, GL14 中也有相关方法,但由于几乎不使用则不进行介绍。

 到这里,大多数操作已经设定完毕,最终编写出的渲染部分代码如下:
public void render(CombatEngineLayers layer, ViewportAPI viewport) {
    if (layer == CombatEngineLayers.ABOVE_SHIPS_LAYER) {
      if (Global.getCombatEngine().getPlayerShip() == null) return;
      ShipAPI ship = Global.getCombatEngine().getPlayerShip();
      SpriteAPI sprite = ship.getSpriteAPI();

      Vector2f location = ship.getLocation();
      Vector2f size = new Vector2f(sprite.getHeight() * 0.5f, sprite.getWidth() * 0.5f);
      float facing = ship.getFacing();

      // 开始进行OpenGL绘制,由于改变了矩阵状态,需要对当前矩阵进行保存
      GL11.glPushMatrix();
      // 依次应用变换矩阵
      GL11.glTranslatef(location.x, location.y, 0.0f);
      GL11.glRotatef(facing, 0.0f, 0.0f, 1.0f);

      // 由于游戏问题,此处可以不保存属性,但不要养成这种坏习惯
      // 此处由于防止受先前其他原版或Mod渲染的影响,需关闭 GL_TEXTURE_2D
      GL11.glDisable(GL11.GL_TEXTURE_2D);

      GL11.glEnable(GL11.GL_BLEND);
      GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
      // 对应为 (255, 0, 0, 127)
      GL11.glColor4f(1.0f, 0.0f, 0.0f, 0.5f);

      // 指示开始绘制矩形
      GL11.glBegin(GL11.GL_QUADS);
      // 依次设定顶点坐标
      GL11.glVertex2f(-size.x, -size.y);
      GL11.glVertex2f(-size.x, size.y);
      GL11.glVertex2f(size.x, size.y);
      GL11.glVertex2f(size.x, -size.y);
      // 指示绘制结束
      GL11.glEnd();
      // 释放保存的矩阵,执行其他渲染
      GL11.glPopMatrix();
    }
}

 如果操作没有出错,那么可以看到在游戏中玩家所操控的舰船被披上了一块半透明红色披风:
  
 所示代码仅仅只是其中一种实现方式,你可以尝试使用其他途径达成目标。


(5) 纹理绘制
 显然,仅仅绘制颜色是不够的,我们还需要为这个矩形映射一张贴图。
 为了映射贴图,同样需要定义UV坐标,并且是为每个顶点分别进行定义;在OpenGL中,提供了如下代码供使用:
public static void glTexCoord1f(float s);
public static void glTexCoord1d(double s);

public static void glTexCoord2f(float s, float t);
public static void glTexCoord2d(double s, double t);

public static void glTexCoord3f(float s, float t, float r);
public static void glTexCoord3d(double s, double t, double r);

public static void glTexCoord4f(float s, float t, float r, float q);
public static void glTexCoord4d(double s, double t, double r, double q);
 其中, s\t\r\q 的前三者分别对应 u\v\w ;对于游戏美术而言通常情况下最多只会应用到3D纹理,不会与 x\y\z 冲突,即便是3D资产中也同样不会遇到 w 坐标,故对于纹理坐标通常使用 u\v\w 称呼。
 在这里,由于我们使用的是一种具有宽与高的2D纹理,且float的精度与取值已经足够适应大多数情况,所以这里选择 glTexCoord2f() 方法进行编写。

 结合到先前代码中,目前其中一部分将变成如下情况:
// 依次设定顶点,以及UV坐标
GL11.glTexCoord2f(0.0f, 0.0f);
GL11.glVertex2f(-size.x, -size.y);
GL11.glTexCoord2f(0.0f, 1.0f);
GL11.glVertex2f(-size.x, size.y);
GL11.glTexCoord2f(1.0f, 1.0f);
GL11.glVertex2f(size.x, size.y);
GL11.glTexCoord2f(1.0f, 0.0f);
GL11.glVertex2f(size.x, -size.y);
 其中,对于每个顶点而言,其余顶点属性应当在提交顶点坐标(即调用 glVertex 系列方法)前设定完成,以防出现某些情况下UV映射不正常等结果。

 设定完UV坐标后,我们还需要告诉OpenGL应该在绘制中使用哪一个纹理,则可以使用如下方法:
// 激活纹理单元位置,在固定管线中,最多支持绑定4个单元
GL13.glActiveTexture(int texture);
// 对于 texture 有如下可选值
GL13.GL_TEXTURE0;
GL13.GL_TEXTURE1;
......
GL13.GL_TEXTURE31;

// 使用纹理对象ID绑定纹理
GL11.glBindTexture(int target, int texture);
// 对于 target 参数,有如下可选值
GL11.GL_TEXTURE_1D;
GL11.GL_TEXTURE_2D;
GL12.GL_TEXTURE_3D; 对于我们将要使用的贴图是个 2D纹理 ,所以绑定时需选择 GL11.GL_TEXTURE_2D 作为target,而纹理对象ID则可以使用 SpriteAPI 的实例方法 getTextureId() 获取。
 这一过程也可以使用 SpriteAPI 中的实例方法完成:
void bindTexture(); 另为了拥有良好的状态管理,在结束绘制后强烈建议使用 glBindTexture() 绑定至ID为0的纹理解除绑定,即便原版与其他Mod不这么做。

 最后,整理得到的代码片段如下:
public void render(CombatEngineLayers layer, ViewportAPI viewport) {
    if (layer == CombatEngineLayers.ABOVE_SHIPS_LAYER) {
      if (Global.getCombatEngine().getPlayerShip() == null) return;
      ShipAPI ship = Global.getCombatEngine().getPlayerShip();
      SpriteAPI sprite = ship.getSpriteAPI();

      Vector2f location = ship.getLocation();
      Vector2f size = new Vector2f(sprite.getHeight() * 0.5f, sprite.getWidth() * 0.5f);
      // 考虑到只能使用OpenGL1.5的远古设备不支持NPOT纹理,原版的纹理载入是设定为留空至2的n次幂尺寸的,所以通常不能直接取1
      // 至于这些设备能不能启动Windows XP/7/10/11,这里不做讨论
      // 这里不考虑这点,直接将uv坐标取全
      Vector2f uv = new Vector2f(1.0f, 1.0f);
      float facing = ship.getFacing();

      // 开始进行OpenGL绘制,由于改变了矩阵状态,需要对当前矩阵进行保存
      GL11.glPushMatrix();
      // 依次应用变换矩阵
      GL11.glTranslatef(location.x, location.y, 0.0f);
      GL11.glRotatef(facing, 0.0f, 0.0f, 1.0f);

      // 由于游戏问题,此处可以不保存属性,但不要养成这种坏习惯
      // 此处需打开 GL_TEXTURE_2D 以使用2D纹理
      GL11.glEnable(GL11.GL_TEXTURE_2D);
      
      // 纹理单元0是默认打开的,此时可以不使用该方法
      GL13.glActiveTexture(GL13.GL_TEXTURE0);
      GL11.glBindTexture(GL11.GL_TEXTURE_2D, sprite.getTextureId());
      // 可选其一
      //sprite.bindTexture();

      GL11.glEnable(GL11.GL_BLEND);
      GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
      // 对应为 (255, 0, 0, 127)
      GL11.glColor4f(1.0f, 0.0f, 0.0f, 0.5f);

      // 指示开始绘制矩形
      GL11.glBegin(GL11.GL_QUADS);
      // 依次设定顶点,以及UV坐标
      GL11.glTexCoord2f(0.0f, 0.0f);
      GL11.glVertex2f(-size.x, -size.y);
      GL11.glTexCoord2f(0.0f, uv.y);
      GL11.glVertex2f(-size.x, size.y);
      GL11.glTexCoord2f(uv.x, uv.y);
      GL11.glVertex2f(size.x, size.y);
      GL11.glTexCoord2f(uv.x, 0.0f);
      GL11.glVertex2f(size.x, -size.y);
      // 指示绘制结束
      GL11.glEnd();
      // 释放当前GL上下文绑定的 GL_TEXTURE_2D 对象
      GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
      // 释放保存的矩阵,执行其他渲染
      GL11.glPopMatrix();
    }
}

 启动游戏运行代码,可以看到如下场面:
  
 尽管不太明显且不太对劲,但这就是正常运行时渲染出来的图形。


# 没了
 至此,文章结束,感谢你的观看。

 若有错误之处,欢迎指出。




=== END ===

编写于:2023/12/24
编写者:ShioZakana
#fee7bb











页: [1]
查看完整版本: 【Modding基础】"SpriteAPI"与"Fixed Rendering Pipeline"