Libgdx New 3D API 教程之 -- 使用Libgdx创建Shader
来源:互联网 发布:数据库删除一行数据 编辑:程序博客网 时间:2024/05/22 05:19
This blog is a chinese version of xoppa's Libgdx new 3D api tutorial. For English version, please refer to >>LINK<<
这篇教程主要讲怎样利用Libgdx 3D API来创建,并使用Shader相关的基础知识. 我们会看到如何通过DefaultShader使用一段自定义的GLSL。然后我们还会讲到创建一个自定义的shader.
在前面我们已经讲过,Shader是负责渲染Renderable对象的。Libgdx提供的DefaultShader,提供了渲染的最基本需要。然而,对于一些高级的渲染,比如一些特效,你可能就需要自定义shader。
在我们深入之前,先看一下前面教程中写到的例子:
public class ShaderTest implements ApplicationListener { public PerspectiveCamera cam; public CameraInputController camController; public Shader shader; public RenderContext renderContext; public Model model; public Lights lights; public Renderable renderable; @Override public void create () { lights = new Lights(); lights.ambientLight.set(0.4f, 0.4f, 0.4f, 1f); lights.add(new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f)); cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); cam.position.set(2f, 2f, 2f); cam.lookAt(0,0,0); cam.near = 0.1f; cam.far = 300f; cam.update(); camController = new CameraInputController(cam); Gdx.input.setInputProcessor(camController); ModelLoader modelLoader = new G3dModelLoader(new JsonReader()); model = modelLoader.loadModel(Gdx.files.internal("data/invaders.g3dj")); NodePart blockPart = model.getNode("ship").parts.get(0); renderable = new Renderable(); blockPart.setRenderable(renderable); renderable.lights = lights; renderable.worldTransform.idt(); renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1)); shader = new DefaultShader(renderable.material, renderable.mesh.getVertexAttributes(), true, false, 1, 0, 0, 0); shader.init(); } @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); renderContext.begin(); shader.begin(cam, renderContext); shader.render(renderable); shader.end(); renderContext.end(); } @Override public void dispose () { shader.dispose(); model.dispose(); } @Override public void resume () {} @Override public void resize (int width, int height) {} @Override public void pause () {} @Override public void dispose () {}}注意我改了类的名字(ShaderText),还使用了一个简单的方法(setRenderable)来方便我设置renderable值。第一次创建shader时,用一个尽量简单的renderable更好一些。所以,我们来改一点代码:
public void create () { cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); cam.position.set(2f, 2f, 2f); 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); NodePart blockPart = model.nodes.get(0).parts.get(0); renderable = new Renderable(); blockPart.setRenderable(renderable); renderable.lights = null; renderable.worldTransform.idt(); renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1)); shader = new DefaultShader(renderable.material, renderable.mesh.getVertexAttributes(), false, false, 1, 0, 0, 0); shader.init();}
这里,我们移除了灯光的对象,将renderable的灯光设定为空,表示这场景里没有灯光。还有一点,在构建DefaultShader的时候,我瘵灯光的标记也设置成了false。之后,我们移除了ModelLoader,取而代之的是,使用了一个我们一早用过的ModelBuilder,我们要通过它创建一个球体。球体的边界长是[2, 2, 2],我们赋予了这个球空的材质,然后每一个顶点都有位置信息,法线信息,和纹理映射属性。
如果你启用了OpenGL ES 2.0,并且运行这个测试,你将会看到,一个超无聊的球:
事实上,它看起来充其量就是一个圆。为了让大家看清楚我们这里渲染的是球而不是圆,可以做下面这样的设置:
renderable.primitiveType = GL20.GL_POINTS;
再运行一下代码:
你可以通过鼠标的拖动来旋转camera。
现在,我们可以看到这个球体的全部顶点了。如果仔细看,就可以发现,这个球是由20个大小渐进的圆组成(从底部到顶部),而每个圆都包含了20个点(围绕着Y轴)。这些正对应着,我们在创建这个球体时,指定的参数divisionsU和divisionsV。我假设你对vertices和meshes这些概念都熟悉了,所以这里不多深入。但是要记得vertex(就是上图中那些点),和fragment(mesh中每一个可见的像素点).
继续之前,记得把后加的那一句删掉(renderable.primitiveType = GL20.GL_POINTS;)。好吧,又回到之前那个超无聊的圆了。
现在,我们要改一改那个default shader,让我们的球变得生动一点。我们需要两个glsl文件,定义shader代码。第一个,将作用于我们球体中的每一个vertex,另一个作用于球体的每一个像素点(fragment)。在assets文件夹中,创建两个空文件,分另命名为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; varying vec2 v_texCoord0; void main() { v_texCoord0 = a_texCoord0; gl_Position = u_projTrans * u_worldTrans * vec4(a_position, 1.0);}
我们首先定义了三个acctribtes:a_position, a_normal 和 a_texCoord0. 这些将被设置为每一个顶点的position(位置), normal(法线), 和texture coordinate(纹理坐标)。
接下来,我们定义了两个uniform,u_worldTrans用来接收renderable.transform值,而u_projTrans被设置为cam.combined值。
注意这些命名都定义在了DefaultShader类中,一会就说到。
最后,我们定义了一个varying: v_texCoord0, 用于将a_texCoord0传递给fragment shader.
main方法是对每一个顶点都会被调用的。在这里,我们将a_texCoord0的值赋给了v_texCoord0,然后计算了顶点在屏幕上的位置。接下来,我们来看一下text.fragment.glsl文件:
#ifdef GL_ES precision mediump float;#endif varying vec2 v_texCoord0; void main() { gl_FragColor = vec4(v_texCoord0, 0.0, 1.0);}
首先,在定义了GL_ES的情况下,我们设置了precision。接下来,定义了v_texCoord0,跟之前在vertex shader中一样。
main函数中,我们把每一点的x坐标设置为红色分量,把y坐标值,设置为了绿色变量。(所以得到一个渐变色的球,这里如果不熟悉,可以在练习时把v_texCoord0分开成0.0, 0.0的二维坐标,然后每一个值都改一改,看看结果。其实这就是一个RGBA)。
我们有了glsl文件了,现在让我们利用这两个文件生成自定义的Shader:
public void create () { ... String vert = Gdx.files.internal("data/test.vertex.glsl").readString(); String frag = Gdx.files.internal("data/test.fragment.glsl").readString(); shader = new DefaultShader(vert, frag, renderable.material, renderable.mesh.getVertexAttributes(), false, false, 1, 0, 0, 0); shader.init();}
我们从那两个文件中读取string到变量中,然后生成DefaultShader。运行一下:
看起来不错,每一点的x轴与y轴的坐标,分别表示了红色与绿色的分量。现在看,只用几行代码,你就可以利用DefaultShder创建自己的Shader(着色器)了。
不过,这只适用于,你的shader属性与uniform与default shader一致时才可以。换句话说,DefaultShader提供了GLSL上下文环境,可以运行你自定义的GLSL代码。
现在看看这里面的机制。
我们刚刚写的GLSL代码是运行在GPU上面的。设置vertex attributes(顶点属性, 如位置),uniforms(如u_worldTrans),给GPU提供mesh,或者还有可选项textures什么的,这些都是CPU扔给GPU的。所以GPU和CPU是一起合作来渲染对象的。如在CPU端绑定了纹理,那GPU不渲染出来是不合理的,或者在GPU端使用到一个uniform,那CPU端需要预先设置好。在Libgdx中,CPU和GPU在一起工作,构成了Shader。它会做所有渲染对象所需要做的。
这里可能有些不清楚,因为大多数书籍文章中都说shader只影响到GPU。可是在Libgdx中,GPU负责的部分被称为ShaderProgram,并且一个Shader是GPU和CPU两部分的组合,大多数情况下,Shader都会使用到ShaderProgram,但不是一定的。
现在来自定义一个Shader,取代DefaultShader,所以新建一个TestShader的类,实现Shader接口:
public class TestShader implements Shader { @Override public void init () {} @Override public void dispose () {} @Override public void begin (Camera camera, RenderContext context) { } @Override public void render (Renderable renderable) { } @Override public void end () { } @Override public int compareTo (Shader other) { return 0; } @Override public boolean canRender (Renderable instance) { return true; }}
在开始写代码之前,看一下最后两个方法。compareTo方法是ModelBatch调用来判断先使用哪一个shader,我们现在还用不着。然后canRender方法用来决定只渲染特定的renderable对象。这个很快就会说到。现在,我们只给他return true;就好。
init方法会在shader生成时被调用一次。这里可以放置ShaderProgram的生成代码:
public class TestShader implements Shader { ShaderProgram program; @Override public void init () { String vert = Gdx.files.internal("data/test.vertext.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()); } @Override public void dispose () { program.dispose(); } ...}
上面的代码中,我们读取了vertex和fragment的GLSL代码文件,并用他们创建了一个ShaderProgram。如果ShaderProgram没有成功创建的话,我们抛出了一个易读的异常,这样方法我们调式GLSL代码。ShaderProgram对象在使用后需要被销毁,所以,在dispose方法中又加了一行。
如果这个shader要用于渲染对象了,那begin方法每个frame都会调用。end方法也会每帧渲染结束后调用。而render方法仅仅会在begin和end方法之前被调用。因此,begin和end方法可以用于处理绑定,和解除绑定我们的ShaderProgram。
public class TestShader implements Shader { ... @Override public void begin (Camera camera, RenderContext context) { program.begin(); } ... @Override public void end () { program.end(); } ...}
begin方法有两个参数,camera和context,在我们的shader调用end之前,这些都要保留使用,所以,我们需要在类里保存下来:
public class TestShader implements Shader { ShaderProgram program; Camera camera; RenderContext context; ... @Override public void begin (Camera camera, RenderContext context) { this.camera = camera; this.context = context; program.begin(); } ...}
之前的ShaderProgram方法有两个uniforms,u_worldTrans和u_projTrans。后者的值取决于camera,因此,我们要在begin方法中设置:
@Overridepublic void begin (Camera camera, RenderContext context) { this.camera = camera; this.context = context; program.begin(); program.setUniformMatrix("u_projTrans", camera.combined);}
u_worldTrans是与renderable对象相关的,所以我们在render方法中设置:
@Overridepublic void render (Renderable renderable) { program.setUniformMatrix("u_worldTrans", renderable.worldTransform);}
现在,uniforms都设置好了,我们不宁设置属性值,与mesh绑定,然后渲染。这些只要调用一个mesh.render()就好:
public class TestShader implements Shader { ... @Override public void render (Renderable renderable) { program.setUniformMatrix("u_worldTrans", renderable.worldTransform); renderable.mesh.render(program, renderable.primitiveType, renderable.meshPartOffset, renderable.meshPartSize); } ...}
新的Shader就好了,我们用一下:
public class ShaderTest extends GdxTest { ... @Override public void create () { ... renderContext = new RenderContext(new DefaultTextureBinder(DefaultTextureBinder.WEIGHTED, 1)); shader = new TestShader(); shader.init(); } ...}
运行结果:
咦,不对哦。我们还没设置RenderContext,让它使用深度测试,所以要改一下。再有,如果我们这么改了,还要启用backface culling。这个启用后,render就不会去渲染背对着相机的点线面。如果说,我们的相机是在球体里面,那你将看不到任何东西,(可以放大一下试试)。
public class TestShader implements Shader { ... @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); }
这回看起来像回事了:
完工,现在我们的shader已经可以完成CPU和GPU两部分工作了。但在今天结束之前,我们再看多点东西:
program.setUniformMatrix("u_worldTrans", renderable.worldTransform);
这里,我们将u_worldTrans的值,设成了renderable.worldTransform。这就意味着,ShaderProgram在每次render被调用时都要去寻址字符串“u_worldTrans”。u_projTrans也是这样。所以我们要通过保存他们的址来,来实现优化:
public class TestShader implements Shader { ShaderProgram program; Camera camera; RenderContext context; int u_projTrans; int u_worldTrans; @Override public void init () { ... u_projTrans = program.getUniformLocation("u_projTrans"); u_worldTrans = program.getUniformLocation("u_worldTrans"); } ... @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); renderable.mesh.render(program, renderable.primitiveType, renderable.meshPartOffset, renderable.meshPartSize); } ...}
现在,我们已经使用libgdx 3d api,创建了最基本的shader。下一篇文件,我们会看一下shader中的材质属性,并且,如何同时使用DefaultShader和你自己创建的Shader.
(泽注:现在xoppa写文章的速度有点慢,这下一章,可能得半个月到一个月。)
- Libgdx New 3D API 教程之 -- 使用Libgdx创建Shader
- Libgdx New 3D API 教程之 -- 使用Libgdx加载3D场景
- Libgdx New 3D API 教程之 -- 使用Libgdx加载模型
- Libgdx New 3D API 教程之 -- Libgdx中使用Materials
- Libgdx New 3D API 教程之 -- Libgdx 3D 基础
- Libgdx New 3D API 教程之 -- Libgdx中的3D frustum culling
- Libgdx New 3D API 教程之 -- 加载3D场景的背后-第一部分
- Libgdx New 3D API 教程之 -- 加载3D场景的背后-第二部分
- Libgdx New 3D API 教程之 -- 与三维物体的交互
- Android 3D引擎之libgdx----Eclipse编译libgdx
- libgdx API之提示
- libgdx 3D
- libgdx之加载obj(3D文件)
- libgdx之加载obj(3D文件)
- 3、使用Gradle创建Libgdx项目
- Basic 3D using libGDX
- [libgdx游戏开发教程]使用Libgdx进行游戏开发(3)-给游戏添加一些控制功能
- [libgdx游戏开发教程]使用Libgdx进行游戏开发(3)-给游戏添加一些控制功能
- Linux下密码输入问题
- Queue队列容器
- 嵌入式学习入门及后续-我的过程
- Linux下ps命令的用法
- UVA748 Exponentiation
- Libgdx New 3D API 教程之 -- 使用Libgdx创建Shader
- 服务器或VPS用户如何用命令创建MYSQL数据库(食用菌百科网提供)
- Linux下tar命令的用法
- HDU2660 Accepted Necklace
- jdom读取xml文件
- ORACLE 10g 下载地址
- Linux误改系统文件
- Hdu 3065 病毒侵袭持续中//Aho-Corasick Automaton
- 设计模式简介