在本教程中,我们将了解如何使用 libGDX 材质。材质是由着色器使用的,因此本教程继续在上一个教程中我们创建了一个自定义着色器的地方开始。
上一篇教程中我们只使用渲染器和着色器来测试我们创建的着色器。这对于测试来说是非常好的,因为很容易看到发生了什么。但是最终您会希望回到我们之前使用的 ModelInstance 和 ModelBatch,因此您可以轻松地使用多个着色器和模型。幸运的是,这很容易实现,所以让我们稍微修改一下代码:
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.utils.Array;
public class MainGame extends ApplicationAdapter{
private PerspectiveCamera cam;
private CameraInputController camCtrol;
private Shader shader;
private Model model;
private Array<ModelInstance> instances = new Array<>();
private ModelBatch modelBatch;
@Override
public void create() {
cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
cam.position.set(0f, 8f, 8f);
cam.lookAt(0, 0, 0);
cam.near = 1f;
cam.far = 300f;
cam.update();
camCtrol = new CameraInputController(cam);
Gdx.input.setInputProcessor(camCtrol);
ModelBuilder modelBuilder = new ModelBuilder();
model = modelBuilder.createSphere(2f, 2f, 2f, 20, 20
, new Material(),
Usage.Position | Usage.Normal | Usage.TextureCoordinates);
for(int x = -5; x <= 5; x += 2) {
for(int z = -5; z <= 5; z += 2) {
instances.add(new ModelInstance(model, x, 0, z));
}
}
shader = new TestShader() ;
shader.init();
modelBatch = new ModelBatch();
}
@Override
public void resize (int width, int height) {
}
@Override
public void render () {
camCtrol.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
for (ModelInstance instance : instances) {
modelBatch.render(instance, shader);
}
modelBatch.end();
}
@Override
public void pause () {
}
@Override
public void resume () {
}
@Override
public void dispose () {
shader.dispose();
model.dispose();
modelBatch.dispose();
}
}
首先,我们将摄像机移动到离原点稍远的地方,这样它就覆盖了我们将要创建的场景。接下来,我们移除了可渲染对象。相反,我们使用一组 ModelInstances,在 XZ 平面的网格上填充球体。我们还删除了渲染上下文,现在改为创建一个 ModelBatch。必须释放 ModelBatch,因此我们还向释放方法添加了一行。如果您遵循前面的教程,那么所有这些更改都应该非常直接。这段代码唯一的新方面是 render 方法:
modelBatch.begin(cam);
for (ModelInstance instance : instances)
modelBatch.render(instance, shader);
modelBatch.end();
现在我们要对每个球体应用不同的颜色。要做到这一点,我们必须首先改变着色器。正如我们之前看到的,着色器由一个 CPU 部分(TestShader.java 文件)和一个 GPU 部分组成。GPU 部分由运行于每个顶点(test.vertex.glsl)的代码和运行于每个片段(test.fragment.glsl)的代码组成。
下面是修改过的 test.vertex.glsl 文件:
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord0;
uniform mat4 u_worldTrans;
uniform mat4 u_projTrans;
void main() {
gl_Position = u_projTrans * u_worldTrans * vec4(a_position, 1.0);
}
没有太大的改变,我们只是删除了 v_texcoord0 的变化,因为片段着色器将不再需要它了。下面是 test.fragment.glsl 文件:
#ifdef GL_ES
precision mediump float;
#endif
uniform vec3 u_color;
void main() {
gl_FragColor = vec4(u_color, 1.0);
}
这里我们也去掉了 v_texcoord0 varying,而是增加了一个 uniform u_color。uniform 用来指定片段的颜色。因此,我们需要在 TestShader 类中设置这个 uniform。下面是完整的 TestShader 代码:
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.GdxRuntimeException;
public class TestShader implements Shader{
private ShaderProgram program;
private Camera cam;
private RenderContext ctx;
private int u_projTrans;
private int u_worldTrans;
private int u_color;
@Override
public void init() {
String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
program = new ShaderProgram(vert, frag);
u_projTrans = program.getUniformLocation("u_projTrans");
u_worldTrans = program.getUniformLocation("u_worldTrans");
u_color = program.getUniformLocation("u_color");
if (!program.isCompiled()) {
throw new GdxRuntimeException(program.getLog());
}
}
@Override
public void dispose() {
program.dispose();
}
@Override
public void begin(Camera camera, RenderContext context) {
cam = camera;
ctx = context;
program.bind();
program.setUniformMatrix(u_projTrans, camera.combined);
ctx.setDepthTest(GL20.GL_LEQUAL);
ctx.setCullFace(GL20.GL_BACK);
}
@Override
public void render(Renderable renderable) {
program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
program.setUniformf(u_color, MathUtils.random(), MathUtils.random(), MathUtils.random());
renderable.meshPart.render(program);
}
@Override
public void end() {
}
@Override
public int compareTo(Shader other) {
return 0;
}
@Override
public boolean canRender(Renderable instance) {
return true;
}
}
这里唯一的变化是,我们添加了一个 u_color 值,用于保存 uniform 的位置,在 render 方法中,我们将其设置为随机颜色。效果如下:
使用随机的颜色并不能给我们很多的控制。我们需要一种方式来告知着色应该用于每个渲染的颜色。要做到这一点,最基本的方法是使用 ModelInstance 的 userData 值。下面是我们如何在 ShaderTest 中做到这一点:
public void create () {
...
for (int x = -5; x <= 5; x+=2) {
for (int z = -5; z<=5; z+=2) {
ModelInstance instance = new ModelInstance(model, x, 0, z);
instance.userData = new Color((x+5f)/10f, (z+5f)/10f, 0, 1);
instances.add(instance);
}
}
...
}
在这里,我们只需将 userData 设置为要在着色器中使用的颜色,在本例中,着色器的颜色取决于实例的位置。接下来我们需要在着色器中使用这个值:
@Override
public void render(Renderable renderable) {
program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
Color color = (Color)renderable.userData;
program.setUniformf(u_color, color.r, color.g, color.b);
renderable.meshPart.render(program);
}
因此,可以使用 userData 值将数据传递给着色器。但是,如果你有多个 uniform,这可能会变得非常混乱,如果你使用多个着色器,这将是一个更痛苦的跟踪uniform 值。我们需要一种更好的方法来设置模型实例的 uniform 值。
首先,快速浏览一下我们在这里要实现的目标。我们上面的着色器有三个 uniform(u_projtrans,u_worldtrans 和 u_color)。第一个取决于相机,第二个(renderable.worldTransform)和第三个取决于渲染器(Renderable)。一般来说,你可以把你的 uniform 分成三组:
请注意,这些组中不仅包括uniform。例如,顶点属性始终是可渲染(MeshPart)的特定值。渲染上下文的深度测试和剔除面值,就像我们在着色器的 begin ()方法中设置的那样,是着色器的全局值。所有这些设置和值一起定义运行 GLSL 的上下文。
在创建 Shader (CPU 部分)时,您应该始终记住哪个值属于哪个组,以及您希望它们被更改的频率。例如,ShadowMap 纹理可以是全局值或环境值,但不太可能是特定值。
对于本教程,我们将只看特定的值,如上面的 u_color uniform。这就是材质的来源。
材质只包含特定的值。可渲染的 MeshPart 定义了应该呈现的形状。同样,材质定义了形状应该如何渲染(例如应用在形状上的颜色) ,而不管它的环境如何。一个可渲染的总是有一个材质(它必须永远不能为空)。一个材料基本上是一系列的材料属性:
class Material {
Array<Material.Attribute> attributes = new Array<Attribute>();
...
}
注意,虽然材质不能为空,但材质本身可以为空
在它最简单的形式中,Attribute 描述了应该将哪个值设置为哪个 uniform 的值。当然,值的类型可以有所不同,例如,uniform 可以是颜色、浮动或纹理。因此,必须为每个特定类型扩展属性。配备了大多数基本类型,例如:
package com.badlogic.gdx.graphics.g3d.materials;
...
public class ColorAttribute extends Attribute {
public final Color color = new Color();
...
}
同样,还有 FloatAttribute 和 TextureAttribute (以及其他一些属性):
colorAttribute.color.set(Color.RED);
为了指定 uniform,每个属性都有一个类型值:
public class Material implements Iterable<Attribute>, Comparator<Attribute> {
public static abstract class Attribute {
public final long type;
...
}
...
}
就像着色器只能包含一个同名的 uniform 属性一样,材质也只能包含一个具有相同类型值的属性。但是当一个 uniform 的名称只用于一个着色器时,一个材质属性可以被许多着色器使用。因此,材质属性独立于着色器。例如,在上面的着色器中,我们有一个称为 u_color 的 uniform 颜色。如果我们有一个只定义颜色的属性,那么它将是模糊的。相反,我们需要更好地定义材质属性的实际用途,例如“此属性指定整个完全不透明的漫反射颜色”。LibGDX 已经包含了这样一个 material 属性,它可以按照以下方式构造:
ColorAttribute attribute = new ColorAttribute(ColorAttribute.Diffuse, Color.RED);
为了方便起见,你也可以这样创建属性:
ColorAttribute attribute = ColorAttribute.createDiffuse(Color.RED);
这里是 ColorAttribute.Diffuseccolor 是类型值。因此,从材料中获取属性也可以用类似的方法来完成:
Material.Attribute attribute = material.get(ColorAttribute.Diffuse);
注意,根据定义,这个属性的类型为 ColorAttribute。漫反射必须始终浇注到色彩属性。(例如,你不能调用 new TextureAttribute(ColorAttribute.Diffuse);) 因此,可以安全地将属性强制转换回 ColorAttribute:
ColorAttribute attribute = (ColorAttribute)material.get(ColorAttribute.Diffuse);
让我们实践一下,修改MainGame类create方法中的代码:
public void create () {
...
for (int x = -5; x <= 5; x+=2) {
for (int z = -5; z<=5; z+=2) {
ModelInstance instance = new ModelInstance(model, x, 0, z);
ColorAttribute attr = ColorAttribute.createDiffuse((x+5f)/10f, (z+5f)/10f, 0, 1);
instance.materials.get(0).set(attr);
instances.add(instance);
}
}
...
}
在这里,我们创建一个类型为 ColorAttribute 的新 ColorAttribute。根据网格上的位置使用不同的颜色进行漫射。接下来,我们向实例的第一个(也是唯一的)材质添加颜色。请注意,这个方法被称为 set (而不是 add) ,因为它将覆盖具有相同类型的任何现有属性。
现在改变 TestShader 来使用这个值来设置 u_color uniform:
@Override
public void render(Renderable renderable) {
program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
Color color = ((ColorAttribute)renderable.material.get(ColorAttribute.Diffuse)).color;
program.setUniformf(u_color, color.r, color.g, color.b);
renderable.meshPart.render(program);
}
这里我们获取 material 类型的 ColorAttribute.Diffuse 属性。将其强制转换为 ColorAttribute 并获取其颜色值。接下来,使用该颜色值设置 u_color 。如果运行此代码,您将看到它呈现的效果与以前完全相同。但是现在我们用material 代替了 userData。
如果你仔细看一下我们刚刚改变的渲染方法,你会发现如果材质不包含类型 ColorAttribute.Diffuse 的属性,那么渲染方法将会严重失败。我们可以添加一个检查,例如:
ColorAttribute attr = (ColorAttribute)renderable.material.get(ColorAttribute.Diffuse);
if (attr != null)
...
else
...
在某些情况下,这是非常有用的。但是在其他情况下(比如我们的着色器) ,对于特定的渲染使用另一个着色器(或者默认着色器)可能会更好。通过实现 canRender 方法,我们可以确保我们的着色器只用于包含特定材质属性的可渲染材质。让我们把它添加到 TestShader 中:
@Override
public boolean canRender(Renderable renderable) {
return renderable.material.has(ColorAttribute.Diffuse);
}
现在,只有材质包含 ColorAttribute.Diffuse 时才会使用着色器。否则,ModelBatch 将回落到默认着色器。
在创建更复杂的着色器时,您可能需要默认不包含的材质属性。因此,让我们创建一个更复杂(但仍然非常简单)的着色器。首先,恢复 v_texcoord0 对顶点着色器的变化(test.vertex.glsl) :
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord0;
uniform mat4 u_worldTrans;
uniform mat4 u_projTrans;
varying vec2 v_texCoord0;
void main() {
v_texCoord0 = a_texCoord0;
gl_Position = u_projTrans * u_worldTrans * vec4(a_position, 1.0);
}
接下来,更改片段着色器以使用纹理坐标来指定颜色(test.fragment.glsl) :
#ifdef GL_ES
precision mediump float;
#endif
uniform vec3 u_colorU;
uniform vec3 u_colorV;
varying vec2 v_texCoord0;
void main() {
gl_FragColor = vec4(v_texCoord0.x * u_colorU + v_texCoord0.y * u_colorV, 1.0);
}
在这里,我们使用两种 uniform 来创建像素颜色,而不是一种 uniform。一种颜色取决于 x (u)纹理坐标,另一种取决于 y (v)纹理坐标。最后,我们需要修改 TestShader 来设置这两种 uniform:
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.utils.RenderContext;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.utils.GdxRuntimeException;
public class TestShader implements Shader{
private ShaderProgram program;
private Camera cam;
private RenderContext ctx;
private int u_projTrans;
private int u_worldTrans;
private int u_colorU;
private int u_colorV;
@Override
public void init() {
String vert = Gdx.files.internal("data/test.vertex.glsl").readString();
String frag = Gdx.files.internal("data/test.fragment.glsl").readString();
program = new ShaderProgram(vert, frag);
u_projTrans = program.getUniformLocation("u_projTrans");
u_worldTrans = program.getUniformLocation("u_worldTrans");
u_colorU = program.getUniformLocation("u_colorU");
u_colorV = program.getUniformLocation("u_colorV");
if (!program.isCompiled()) {
throw new GdxRuntimeException(program.getLog());
}
}
@Override
public void dispose() {
program.dispose();
}
@Override
public void begin(Camera camera, RenderContext context) {
cam = camera;
ctx = context;
program.bind();
program.setUniformMatrix(u_projTrans, camera.combined);
ctx.setDepthTest(GL20.GL_LEQUAL);
ctx.setCullFace(GL20.GL_BACK);
}
@Override
public void render(Renderable renderable) {
program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
Color colorU = ((ColorAttribute)renderable.material.get(ColorAttribute.Diffuse)).color;
Color colorV = Color.BLUE;
program.setUniformf(u_colorU, colorU.r, colorU.g, colorU.b);
program.setUniformf(u_colorV, colorV.r, colorV.g, colorV.b);
renderable.meshPart.render(program);
}
@Override
public void end() {
}
@Override
public int compareTo(Shader other) {
return 0;
}
@Override
public boolean canRender(Renderable renderable) {
return renderable.material.has(ColorAttribute.Diffuse);
}
}
我们获得了 u_colorU 和 u_colorV 的位置。然后我们将 u_colorU 设置为漫反射颜色属性的颜色。然后我们将 u_colorV 设置为 Color.BLUE。
在着色器中,我们现在根据 x 纹理坐标对颜色使用漫反射颜色属性。我们使用 Color.BLUE 作为颜色,这取决于 y 纹理坐标。但是如果两者都可以使用 material 属性进行配置就更好了。因此我们需要创建两个材质属性,一个是基于 u 值的漫反射颜色,另一个是基于 v 值的漫反射颜色。最简单的方法就是扩展 ColorAttribute 类并注册附加的类型值,所以让我们这样做:
public class TestShader implements Shader {
public static class TestColorAttribute extends ColorAttribute {
public final static String DiffuseUAlias = "diffuseUColor";
public final static long DiffuseU = register(DiffuseUAlias);
public final static String DiffuseVAlias = "diffuseVColor";
public final static long DiffuseV = register(DiffuseVAlias);
static {
Mask = Mask | DiffuseU | DiffuseV;
}
public TestColorAttribute (long type, float r, float g, float b, float a) {
super(type, r, g, b, a);
}
}
...
}
因为这是一个很小的类,所以我没有创建一个新的 java 文件,而是创建了 TestShader 的静态子类。类扩展了 ColorAttribute,因为这是我们需要注册的属性类型。在这个类中,我们有一些公共的最终静态成员。第一个是 DiffuseUAlias,它是我们将要定义的属性类型的名称。在调用 attribute.toString ()时返回此值。在下一行中,我们将该名称注册为属性类型。Register 方法返回类型值(全局唯一) ,我们使用它来初始化 DiffuseU 值。这允许我们使用 TestColorAttribute。作为属性类型,就像 ColorAttribute.Diffuse 值一样。接下来,我们对 DiffuseVAlias 执行相同的操作,并且 register 是一个 DiffuseV 类型。
我们现在已经注册了两个新的材质属性 DiffuseU 和 DiffuseV。但是由于我们扩展了 ColorAttribute 类,我们还需要通知类接受这些属性。这是在下一行完成的(Mask = Mask | DiffuseU | DiffuseV)。最后我们实现了一个构造函数,这样我们就可以实际构造新创建的材质属性。
让我们使用这两个新的材质属性,首先修改 MainGame 类:
public void create(){
...
for(int x = -5; x <= 5; x += 2) {
for(int z = -5; z <= 5; z += 2) {
ModelInstance instance = new ModelInstance(model, x, 0, z);
ColorAttribute attrU = new TestColorAttribute(TestColorAttribute.DiffuseU, (x+5f)/10f, 1f - (z+5f)/10f, 0, 1);
instance.materials.get(0).set(attrU);
ColorAttribute attrV = new TestColorAttribute(TestColorAttribute.DiffuseV, 1f - (x+5f)/10f, 0, (z+5f)/10f, 1);
instance.materials.get(0).set(attrV);
instances.add(instance);
}
}
...
}
现在我们创建两个 TestColorAttributes,一个类型为 DiffuseU,另一个类型为 DiffuseV。我们根据网格的位置来设置它们的颜色。最后我们把它们添加到材质中,就像我们之前做的那样。现在我们需要更改 TestShader 来实际使用它们:
@Override
public void render(Renderable renderable) {
program.setUniformMatrix(u_worldTrans, renderable.worldTransform);
Color colorU = ((ColorAttribute)renderable.material.get(TestColorAttribute.DiffuseU)).color;
Color colorV = ((ColorAttribute)renderable.material.get(TestColorAttribute.DiffuseV)).color;
program.setUniformf(u_colorU, colorU.r, colorU.g, colorU.b);
program.setUniformf(u_colorV, colorV.r, colorV.g, colorV.b);
renderable.meshPart.render(program);
}
在渲染方法中,我们获取这两个属性的颜色并相应地设置统一的颜色。和以前差不多。现在,如果你运行这个程序,它会看起来像这样:
这可不是我们所期待的。这是因为我们还没有更新 canRender 方法。因为材质现在不包含 ColorAttribute。漫反射属性,则 ModelBatch 返回到默认着色器。尽管我们在调用 modelBatch.render (实例,着色器)时显式地指定了着色器,但是 ModelBatch 还是防止我们使用不能使用的着色器。我们需要修改 TestShader 类的 canRender 方法,通知 ModelBatch 它现在接受新的 material 属性:
public boolean canRender (Renderable renderable) {
return renderable.material.has(TestColorAttribute.DiffuseU | TestColorAttribute.DiffuseV);
}
到了这里,我们现在可以控制我们的 uniform 与自定义材质属性。
你可能已经注意到我使用按位运算符来组合多个材质属性类型。例如:
return renderable.material.has(TestColorAttribute.DiffuseU | TestColorAttribute.DiffuseV);
只有当材质同时具有 DiffuseU 属性和 DiffuseV 属性时,才会返回 true。现在,我们不会进一步研究这个问题,但是请记住,可以将物质属性结合起来进行更快的比较。
最后,材质属性不必指定 uniform 值的。例如,LibGDX 附带了属性类型 IntAttribute.CullFacee,它可以与 context.setCullFace (...) 一起使用,不需要设置 uniform 的值。同样,属性不必指定单个值。例如,在上面的着色器中,我们使用了两个颜色属性,DiffuseU 和 DiffuseV。更好的方法是创建一个包含两个颜色值的属性,例如:
public class DoubleColorAttribute extends Attribute {
public final static String DiffuseUVAlias = "diffuseUVColor";
public final static long DiffuseUV = register(DiffuseUVAlias);
public final Color color1 = new Color();
public final Color color2 = new Color();
protected DoubleColorAttribute (long type, Color c1, Color c2) {
super(type);
color1.set(c1);
color2.set(c2);
}
@Override
public Attribute copy () {
return new DoubleColorAttribute(type, color1, color2);
}
@Override
protected boolean equals (Attribute other) {
DoubleColorAttribute attr = (DoubleColorAttribute)other;
return type == other.type && color1.equals(attr.color1) && color2.equals(attr.color2);
}
@Override
public int compareTo (Attribute other) {
if (type != other.type)
return (int) (type - other.type);
DoubleColorAttribute attr = (DoubleColorAttribute) other;
return color1.equals(attr.color1)
? attr.color2.toIntBits() - color2.toIntBits()
: attr.color1.toIntBits() - color1.toIntBits();
}
}