This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<
在这一章中,你将看到在Libgdx中是如何使用Materials的。Material是基于Shader的,所以这一节其实是上一节教程的续,上一节中,我们自定义了一个shader。如果你还没有自定义shader,我建议你先看一下上一章。
之前,我们仅通过一个Renderable和一个shader,来测试我们的shader。因为简单明了,用来测试是很好的。但真正用起来的话,你还是会用到之前见过的ModelInstance和ModelBatch,这样你可以使用多个shader和model。还好,这些都很容易,我们改一改代码。下面给出ShaderText.java的完整代码作为参考,然后我们会讲讲改了什么:
public class ShaderTest implements ApplicationListener {
public PerspectiveCamera cam;
public CameraInputController camController;
public Shader shader;
public Model model;
public Array<ModelInstance> instances = new Array<ModelInstance>();
public 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 = 0.1f;
cam.far = 300f;
cam.update();
camController = new CameraInputController(cam);
Gdx.input.setInputProcessor(camController);
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 render () {
camController.update();
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
modelBatch.begin(cam);
for (ModelInstance instance : instances)
modelBatch.render(instance, shader);
modelBatch.end();
}
@Override
public void dispose () {
shader.dispose();
model.dispose();
modelBatch.dispose();
}
@Override
public void resume () {
}
@Override
public void resize (int width, int height) {
}
@Override
public void pause () {
}
}
首先,我们我们让camera离原点远了些,这样才能看到整个画面。然后,删掉了renderable对象。并用一组ModelInstances取而代之,我们会放一些球体在这个数组中,然后让他们放在一个以XZ轴为平面的格子里。我们还用ModelBatch取代了RenderContext。ModelBatch需要销毁,所以在dispose方法中加了相应的方法。如果你一直有看之前的教程,那这些都讲到过。只有一处是新的,在render方法中。
modelBatch.begin(cam);
for (ModelInstance instance : instances)
modelBatch.render(instance, shader);
modelBatch.end();
这里,我们为ModelInstances指定了渲染时要使用的shader。这是给ModelBatch指定shader的最简单的方式。所以,运行一下看看:
现在,我们要给每一个球指定不同的颜色,首先要改一改shader。我们以前介绍过,shader包含CPU和GPU两部分,而GPU部分是由处理vertex和fragment的代码组成。
下面是改好的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数组,因为接下来fragment shader用不着了。然后再看看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,相应的添加了一个uniform:u_color。这个uniform是用来指定fragment颜色的。所以,我们需要在TestShader类中,指定uniform。下面给出参考代码:
public class TestShader implements Shader {
ShaderProgram program;
Camera camera;
RenderContext context;
int u_projTrans;
int u_worldTrans;
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);
if (!program.isCompiled())
throw new GdxRuntimeException(program.getLog());
u_projTrans = program.getUniformLocation("u_projTrans");
u_worldTrans = program.getUniformLocation("u_worldTrans");
u_color = program.getUniformLocation("u_color");
}
@Override
public void dispose () {
program.dispose();
}
@Override
public void begin (Camera camera, RenderContext context) {
this.camera = camera;
this.context = context;
program.begin();
program.setUniformMatrix(u_projTrans, camera.combined);
context.setDepthTest(true, GL20.GL_LEQUAL);
context.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.mesh.render(program,
renderable.primitiveType,
renderable.meshPartOffset,
renderable.meshPartSize);
}
@Override
public void end () {
program.end();
}
@Override
public int compareTo (Shader other) {
return 0;
}
@Override
public boolean canRender (Renderable instance) {
return true;
}
}
仅有一处改动,我们添加了一个u_color,它保存的是u_color这个uniform的地址。而在render中,我们给它设定了一个随机的颜色。运行结果是这样的:
使用随机的颜色并没有让我们更好的控制shader,我想要一种方法,可以给每一个renderable对象指定我想要的颜色。最基本的做法是使用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,例子中,就是基于每一个instance位置的渐变色。接下来,通过shader使用这些值:
public void render (Renderable renderable) {
...
Color color = (Color)renderable.userData;
program.setUniformf(u_color, color.r, color.g, color.b);
...
}
使用userData值,将颜色传给了Shader。不过如果你有多个uniform,然后还要使用多个shader的时候,这就会变得乱七八糟,使用这些uniform会变得痛苦不堪。我们需要一个更好的方式来设置model instance中uniform的值。
我们的目标是:我们的shader中一共有3个uniform(u_projTrans, u_wordTrans, u_color)。第一个取决于camera,第二个(renderable.worldTransform)和第三个取决于renderable对象。通常情况下,你可以将uniforms分为三类:
Global(全局):这些值你可以在shader的begin方法中设置。这些是所有renderable通用的,而且中间不会修改。比如u_projTrans就是。
Environmental(环境):这些不是全局的,但也不是与model instance直接相关。大多数时候,这些可能跟instance在场景中的位置有关,比如灯光(renderable.lights)就是一个典型的环境uniform。
Specific(指定):这个就是每个ModelInstance(NodePart)独有的了。独立于场景或场景中的位置,这些值会经常被使用。比如u_worldTrans和u_color。
*这里所说的指定值是说每一个ModelInstance特定的值。你也可以理解为位置或model值。
注意这三组值不仅仅包含uniform。比如顶点属性就是renderable的指定值。在begin方法中设置的深度测试和cull face(这个不知道怎么译),就是全局的。这些设置和值都是GLSL运行上下文中的内容。
当创建Shader的时候(CPU部分),你要一直留意着哪些变量是属于哪一类的,并且,它们是否经常改变。比如ShadowMap纹理就可能是全局的,或环境的,怎么看都不像是指定的。
这篇教程中,我们只看一下specific值,好像之前的u_color一样。这才是跟材料有关的东西。
Material仅仅包含指定值。MeshPart定义了一个渲染一个Renderable的图形。类似的,material定义了如何去渲染这些图形,比如图形的颜色之类,不考虑环境因素。一个renderable总会包含一个material(不可能为空)。一个material本质上来说,就是一个材质属性的数组:
class Material { Array<Material.Attribute> attributes = new Array<Material.Attribute>(); ... }
*上文所说的材质不能为空,是指材质对象不能为null,但是可以是一个空的材质。
简单来说,Material.Attribute描述了值与uniform的设置关系。当然数据类型都可以不同,比如一个color,float,或texture的uniform。因此,Material.Attribute必须要继承每一个指定的类型。Libgdx提供了大部分基本类型,比如:
package com.badlogic.gdx.graphics.g3d.materials; ... public class ColorAttribute extends Material.Attribute { public final Color color = new Color(); ... }
还有FloatAttribute和TextureAttribute等类型。因此指定数据就很简单了:
colorAttribute.color.set(Color.RED);
指定uniform时,每一个Material.Attribute都会有一个type值:
public class Material implements Iterable<Material.Attribute>, Comparator<Material.Attribute> { public static abstract class Attribute { public final long type; ... } ... }
一个Shader中,uniform的名字不能重复,类似的,Material中的每一种数据类型也只能定义一个attribute。但是一个uniform只能应用于一个shader,可是material attribute可以很多shader共用。因此material attribute独立于shader。比如,在我们上面定义的shader中,有一个名为u_color的uniform。如果一个attribute定义了一种颜色,这个概念就模糊了。我们需要更准确的定义,这个颜色将会起到什么作用,比如这是一个环境光的颜色。Libgdx已经定义了这样一个材质,可以像这样构建:
ColorAttribute attribute = new ColorAttribute(ColorAttribute.Diffuse, Color.RED);
更简单一点,你可以这样:
ColorAttribute attribute = ColorAttribute.createDiffuse(Color.RED);
ColorAttribute.Diffuse指定了类型,比如,从一个材质中取得环境光的属性:
Material.Attribute attribute = material.get(ColorAttribute.Diffuse);
注意的是,通过ColorAttribute.Diffuse定义的attribute总是可以转换成ColorAttribute。(比如你不能调用 new TextureAttribute(ColorAttribute.Diffuse);这没任何意义)。所以你可以直接转换:
ColorAttribute attribute = (ColorAttribute)material.get(ColorAttribute.Diffuse);
我们把这些代码加到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); ColorAttribute attr = ColorAttribute.createDiffuse((x+5f)/10f, (z+5f)/10f, 0, 1); instance.materials.get(0).set(attr); instances.add(instance); } } ... }
我们使用ColorAttribute.Diffuse创建了一个ColorAttribute,颜色值基于网格的位置。然后将这个颜色加到instance的仅有的第一个材质中。哦,方法名是set而不是add,因为,如果attribute名是相同的,那就会覆盖。
现在,改成利用这个来指定u_color uniform的值。
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); ... }
我们取得material中的ColorAttribute.Diffuse值,转换成ColorAttribute,然后得到它的颜色。然后把这个颜色设置给u_color。如果你现在运行的话,跟原来的效果是一样的,不过我们是通过材质,而非userData来实现这一效果。
再仔细看一下刚刚改的render方法,你会发现,如果material中不包含ColorAttribute.Diffuse属性的话,代码就会出很大的问题。所以,我们最好加一个判断:
ColorAttribute attr = (ColorAttribute)renderable.material.get(ColorAttribute.Diffuse); if (attr != null) ... set the uniform else ... fall back to a default color
有时候这样很有用,但好像我们的shader就没那么好。对于指定的renderable,可能最好的方式是用另一个shader(比如default shader)。我们可以将我们的shader指定给那些包含特定material attribute的renderable对象。像下面这样做:
public boolean canRender (Renderable renderable) { return renderable.material.has(ColorAttribute.Diffuse); }
现在,这个shader就只会有material中包含ColorAttribute.Diffuse属性的时候才会使用了。否则,ModelBatch会使用Default Shader。
要创建一个更复杂的shader,你可能会用到一些非默认的属性值,所以,我们现在就来新建一个,最简单的复杂shader。首先,在test.vertex.glsl中,添加回v_texCoord0:
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); }
然后,改一改fragment shader,通过texture coordinates来指定颜色。
#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而不是一个来指定pixel color。一个取决于x(u)纹理坐标,另一个取决于y(v)纹理坐标。最后我们在TestShader试一下:
public class TestShader implements Shader { ShaderProgram program; Camera camera; RenderContext context; int u_projTrans; int u_worldTrans; int u_colorU; int u_colorV; @Override public void init () { ... u_worldTrans = program.getUniformLocation("u_worldTrans"); u_colorU = program.getUniformLocation("u_colorU"); u_colorV = program.getUniformLocation("u_colorV"); } ... @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.mesh.render(program, renderable.primitiveType, renderable.meshPartOffset, renderable.meshPartSize); } ... }
取得u_colorU和u_colorV的地址,然后把diffuse color赋给u_colorU,把u_colorV设置成Color.BLUE.
在Shader中,我们已经将diffuse color属性设置为基于x纹理坐标的渐变色。并且Color.BLUE也是基于y纹理坐标的。但如果两个属性都通过material attribute来配置的话就好多了。所以,我们新创建两个material attributes. 一个用于基于U值的diffuse color,另一个用于基于V值的diffuse color。最容易的作法是扩展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); } } ... }
这个类太简单了,我没创建新的类,而在TestShader中,写了一个静态类。这个类继承自ColorAttribute,因为这是我们要注册的属性类型。在这个类中,有一些public final static的成员变量。首先是DiffuseUAlias, 这个是我们要定义的attribute类型名,这个值会在调用attribute.toString()时返回。接下来,我们将其注册为一个属性类型。register方法返回了type的值(全局唯一),这个我们用来做DiffuseU的初始值。这使得我们可以通过TestColorAttribute.DiffuseU来使用attribute type。就好像我们以前使用ColorAttribute.Diffuse那样。下面的DiffuseVAlias也是一样。
我们现在已经有了两个material attribute了。但因为我们是扩展ColorAttrbiute类,我们还需要告知这个类接收这些属性。下面的Mask = Mask | DiffuseU | DiffuseV就是做这个的。最后,重写了构建器,这样我们才可以构建新的material attribute.
用一下这两个material attributes。首先改ShaderTest class:
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。我们将其设置为基于网格位置的颜色。最后我们把他们添加到material中,跟以前一样。现在我们需要改变TestShader来使用他们:
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.mesh.render(program, renderable.primitiveType, renderable.meshPartOffset, renderable.meshPartSize); }
在render方法中,我们得到两个attributes颜色值,然后相应的设置给uniforms。现在运行一下:
跟我们的预期不一样,因为还没有改过canReader方法。现在我们的material可没有ColorAttribute.Diffuse这个属性,所以ModelBatch使用了default shader。注算在render时候,通过modelBatch.render(instance, shader)指定shader也没用。ModelBatch限制我们使用不能用的shader。我们需要修改TestShader类中的canRender方法,这样ModelBatch才会接受新的material attributes:
public boolean canRender (Renderable renderable) { return renderable.material.has(TestColorAttribute.DiffuseU | TestColorAttribute.DiffuseV); }
现在看一下运行效果:
好多了,现在我们可以使用自定义的material attributes来控制我们的uniforms。
也许你注意到,我用了位或运算来联合多个material attributes类型。比如:
return renderable.material.has(TestColorAttribute.DiffuseU | TestColorAttribute.DiffuseV);
这个只会在material中包含DiffuseU和DiffuseV两个属性的时候才返回true。现在不作多介绍,但记住material attribute可以这样简单快速的联合在一起。
最后再说一些,给material attributes指定uniform不是必须的。比如Libgdx有一个attribute叫作 IntAttribute.CullFace,可以跟context.setCullFace()一起用,而这并不需要设置一下uniform值。同样,一个attributes也不是一定要有一个它自己的uniform,比如上面的例子,在shader中,我们使用了两个color attributes,DiffuseU和DiffuseV。更好的方法,可以仅用一个attribute来包含这两个值:
public class DoubleColorAttribute extends Material.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); } }