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


    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);
        }
    }






  • 相关阅读:
    C语言I博客作业05
    C语言I博客作业04
    C语言II博客作业04
    C语言II博客作业03
    C语言II博客作业02
    C语言II博客作业01
    学期总结
    C语言I博客作业07
    C语言I博客作业04
    C语言II博客作业04
  • 原文地址:https://www.cnblogs.com/javawebsoa/p/3228750.html
Copyright © 2020-2023  润新知