Libgdx New 3D API 教程之 -- Libgdx中使用Materials

来源:互联网 发布:泉州聚客网络 编辑:程序博客网 时间:2024/05/22 08:20

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 uniformelse    ... 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;#endifuniform 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;@Overridepublic void init () {...u_worldTrans = program.getUniformLocation("u_worldTrans");u_colorU = program.getUniformLocation("u_colorU");u_colorV = program.getUniformLocation("u_colorV");}...@Overridepublic 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);    }}








原创粉丝点击