当前位置: 首页 > news >正文

Android OpenGL粒子特效

在本篇,我们将开启一个新的项目,探索粒子的世界。粒子是一种基本的图形元素,它们通常被表示为一组点。通过巧妙地组合一些基础的物理效果,我们能够创造出许多令人惊叹的视觉效果。想象一下,我们可以模拟一个水滴从喷泉中喷出,然后优雅地落回地面的场景。同样,我们也能模拟出逼真的下雨效果,或者制作出爆炸和烟花的动画。粒子系统的数学原理相对简单,这使得它们很容易被集成到任何三维场景中。

在本章,我们将逐步构建一个粒子系统。首先,我们会介绍创建粒子系统所需的基本要素。接下来,我们将添加喷泉效果,让粒子喷射到空中。此外,我们还会探讨如何通过技术手段,如混合和点精灵,来提升粒子的视觉效果,使它们看起来更加好看。

创建着色器

作为开始,我们将构建一个基础的粒子系统,模拟一个喷泉。这个喷泉可以想象成在灯光下喷发的泉水,或者像烟花表演中的喷泉一样。为了实现这个效果,我们需要处理一些技术细节。

首先,我们需要找到一种方式来在内存中表示所有的粒子。虽然可以使用Java对象数组,但在运行时创建和删除大量对象可能会导致资源消耗过大,并且没有简单的方法将数据传递给OpenGL。因此,我们选择将所有粒子数据嵌入到一个固定大小的数组中。添加粒子时,我们只需增加粒子计数,将数据写入粒子数组,并把变化的内容复制到本地缓冲区。当空间不足时,可以通过在数组开头重新开始来回收空间。

接下来,我们需要一种方法来绘制每个粒子。我们将每个粒子表示为一个顶点,并绘制一组点,每个点都有其独特的位置和颜色。

最后,我们需要一种方法来更新这些粒子。将这些逻辑放入着色器程序中,可以让GPU分担一部分更新工作。对于每个粒子,我们需要存储一个方向向量和一个创建时间。利用创建时间,我们可以计算出粒子自创建以来经过的时间,然后使用这个时间、方向向量和位置来推算出粒子当前的位置。我们将使用一个浮点数来存储时间,并以0.0表示粒子系统开始运行的时间。

有了这些基本需求,我们可以为着色器程序制定一套初始规范。首先,我们需要定义一个uniform变量用于投影矩阵,以及一个用于当前时间的uniform变量,以便着色器计算出每个粒子自创建以来经过的时间。我们还需要定义四个与粒子属性相对应的属性:位置、颜色、方向向量和创建时间。

1.编写顶点着色器

让我们开始给着色器添加代码。继续并在“/res/raw/”文件夹内创建一个名为“particle_vertex_shader.glsl”的新顶点着色器。首先以如下定义作为开始:

uniform mat4 u_Matrix;
uniform float u_Time;attribute vec3 a_Position;
attribute vec3 a_Color;
attribute vec3 a_DirectionVector;
attribute float a_ParticleStartTime;varying vec3 v_Color;
varying float v_ElapsedTime;void main(){v_Color = a_Color;v_ElapsedTime = u_Time - a_ParticleStartTime;vec3 currentPosition = a_Position + (a_DirectionVector * v_ElapsedTime);gl_Position = u_Matrix * vec4(currentPosition,1.0);gl_PointSize = 10.0;
}

这些定义满足了我们的粒子着色器的需求。我们在片段着色器中也需要使用颜色和运行时间,因此我们也为这两个变量创建了两个varying。

在main函数中,我们首先把颜色发送给片段着色器,接着计算粒子从被创建之后运行了多少时间,并且把那个时间也发送给片段着色器。为了计算粒子的当前位置,我们把方向向量与运行时间相乘,并与a_Position相加。运行时间越长,粒子走得越远。

要完成这个着色器代码,我们把粒子用那个矩阵进行投影,而且,因为我们把粒子渲染为一个点,所以把点的大小设成了10个像素。

当我们做数学运算时,要确保不会意外地把w分量弄混乱,这是很重要的。因此,我们将用3分量向量表示位置和方向,只有需要把它与u_Matrix相乘时,才把它转换为完全的4分量向量。这确保上面的数学运算只影响x、y和z分量。

2.编写片段着色器

现在我们可以继续并添加片段着色器了。在与顶点着色器相同的地方创建一个被称为“particle_fragment_shader.glsl”的新文件,并加入如下代码:

precision mediump float;
varying vec3 v_Color;
varying float v_ElapsedTime;void main(){gl_FragColor = vec4(v_Color / v_ElapsedTime,1.0);
}

通过把颜色除以运行时间,这个着色器会使年轻的粒子明亮,而使年老的粒子暗淡。如果发生除以0的情况怎么办?根据规范,这会导致一个不明确的结果,但不会导致着色器程序终止。要一个更加可预测的结果,你可以总是给分母加上一个很小的数。

3.用一个类封装着色器

着色器代码完成了,我们现在可以用一个类封装着色器,它使用我们在本书第一部分所使用的模式。让我们首先给 ShaderProgram加入一些新的常量:

    protected val U_TIME = "u_Time"protected val A_DIRECTION_VECTOR = "a_DirectionVector"protected val A_PARTICLE_START_TIME = "a_ParticleStartTime"

这些新的常量被定义后,我们可以继续加入一个名为“ParticleShaderProgram”的新类,它继承自ShaderProgram,并以如下代码作为这个类的开始:

    private var uMatrixLocation = 0private var uTimeLocation = 0var aPositionLocation = 0var aColorLocation = 0var aDirectionVectorLocation = 0var aParticleStartTimeLocation = 0

接着继续完成这个类的定义:

    constructor(context: Context):super(context, R.raw.particle_vertex_shader,R.raw.particle_fragment_shader1){uMatrixLocation = findUniformLocationByName(U_MATRIX)uTimeLocation = findUniformLocationByName(U_TIME)aPositionLocation = findAttribLocationByName(A_POSITION)aColorLocation = findAttribLocationByName(A_COLOR)aDirectionVectorLocation = findAttribLocationByName(A_DIRECTION_VECTOR)aParticleStartTimeLocation = findAttribLocationByName(A_PARTICLE_START_TIME)uTextureUnitLocation = findUniformLocationByName(U_TEXTURE_UNIT)}fun setUniforms(matrix:FloatArray,elapsedTime:Float){GLES20.glUniformMatrix4fv(uMatrixLocation,1,false,matrix,0)GLES20.glUniform1f(uTimeLocation,elapsedTime)}

封装模式与前面篇章的一致,这里就不再过多赘述。

添加粒子系统

我们现在可以开始创建粒子系统了。让我们创建一个名为“ParticleSystem”的新类,这个类以如下代码开始:

    private val POSITION_COMPONENT_COUNT = 3private val COLOR_COMPONENT_COUNT = 3private val VECTOR_COMPONENT_COUNT = 3private val PARTICLE_START_TIME_COMPONENT_COUNT = 1private val TOTAL_COMPONENT_COUNT = POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT + VECTOR_COMPONENT_COUNT + PARTICLE_START_TIME_COMPONENT_COUNTprivate val STRIDE = TOTAL_COMPONENT_COUNT * BYTES_PER_FLOAT

目前为止,我们只有一些基本定义,它们是为分量计数和粒子之间的跨距添加的。让我们继续构建这个类:

    private var particles:FloatArrayprivate var vertexArray:VertexArrayprivate var maxParticleCount = 0private var currentParticleCount = 0private var nextParticle = 0constructor(maxParticleCount:Int){particles = FloatArray(maxParticleCount*TOTAL_COMPONENT_COUNT)vertexArray = VertexArray(particles)this.maxParticleCount = maxParticleCount}

我们现在有了一个存储粒子的浮点数组和一个VertexArray,VertexArra们要发送给OpenGL的数据,因为数组的大小是固定的,我们还定义了保maxParticleCount。我们将使用currentParticleCount和nextParticle让我们开始构建一个名为addParticle()的新方法:

    fun addParticle(position:Point,color:Int,direction:Vector,particleStartTime:Float){val particleOffset = nextParticle * TOTAL_COMPONENT_COUNTvar currentOffset = particleOffsetnextParticle++if(currentParticleCount < maxParticleCount){currentParticleCount++}if(nextParticle == maxParticleCount){nextParticle = 0}

要创建新的粒子,首先要传入位置、颜色、方向和粒子创建时间。颜色作为Android的颜色定义被传递进来,我们需要用Android的Color类把这个颜色分解为单独的分量。

在给数组加入新的粒子之前,我们需要计算它需要存在哪里。因为所有的粒子都一起存在我们的数组中,它类似于一种无定形的blob(二进制大对象)。为了计算正确的偏移值,我们使用nextParticle存储下一个粒子的编号,其中第一个粒子从0开始编号。然后,我们可以通过把nextParticle与每个粒子的分量计数相乘得到偏移值。我们把这个偏移值存储在 particleOffset和 currentOffset中;我们使用 particleOffset记住新粒子从哪里开始,而用currentOffset记住新粒子的每个属性的位置。

一个新粒子每次被添加进来时,我们就给nextParticle增加1,当到了数组的结尾处,我们就从0开始以便回收最旧的粒子。我们也需要记录有多少个粒子需要被绘制出来,为此,一个新粒子每次被加入的时候,我们都增加currentParticleCount,但要把它限制在最大粒子数内。

我们已经解释了这些操作,接下来让我们把这个新的粒子数据写到数组中:

        particles[currentOffset++] = position.xparticles[currentOffset++] = position.yparticles[currentOffset++] = position.zparticles[currentOffset++] = Color.red(color)/255fparticles[currentOffset++] = Color.green(color)/255fparticles[currentOffset++] = Color.blue(color)/255fparticles[currentOffset++] = direction.xparticles[currentOffset++] = direction.yparticles[currentOffset++] = direction.zparticles[currentOffset++] = particleStartTime

首先存进的是位置,然后是颜色(使用Android的Color类分解出每个分量),紧接着是方向向量,最后是粒子创建时间。Android的Color类返回的分量范围是从0到255,而OpenGL期望的范围是从0到1,因此,通过用每个分量都除以255,我们把它从Android的范围转换为OpenGL期望的范围。

我们还需要把这个新的粒子复制到本地缓冲区,以便OpenGL可以存取这些新的数据,因此,让我们用下面的方法调用完成addParticle():

        vertexArray.updateBuffer(particles,particleOffset,TOTAL_COMPONENT_COUNT)

我们只想复制这些新数据,这样就不会浪费时间去复制那些没有改变的数据,因此我们传递进起始偏移值和那个计数。由于updateBuffer()作为VertexArray内部的一个新方法,我们还需要加入它的定义。为此,我们在类的结尾处加入如下代码:

    fun updateBuffer(vertexData:FloatArray,start:Int,count:Int){floatBuffer.position(start)floatBuffer.put(vertexData,start,count)floatBuffer.position(0)}

现在,我们可以返回 ParticleSystem,并加入一个绑定函数:

    fun bindData(particleProgram:ParticleShaderProgram){var dataOffset = 0vertexArray.setVertexAttribPointer(dataOffset,particleProgram.aPositionLocation,POSITION_COMPONENT_COUNT,STRIDE)dataOffset += POSITION_COMPONENT_COUNTvertexArray.setVertexAttribPointer(dataOffset,particleProgram.aColorLocation,COLOR_COMPONENT_COUNT,STRIDE)dataOffset += COLOR_COMPONENT_COUNTvertexArray.setVertexAttribPointer(dataOffset,particleProgram.aDirectionVectorLocation,VECTOR_COMPONENT_COUNT,STRIDE)dataOffset += VECTOR_COMPONENT_COUNTvertexArray.setVertexAttribPointer(dataOffset,particleProgram.aParticleStartTimeLocation,PARTICLE_START_TIME_COMPONENT_COUNT,STRIDE)}

这又只是一段样板式的代码,它遵循与前面几篇一样的模式,在着色器程序中把顶点数据与正确的属性绑定,并小心遵守我们在addParticle()中所使用的顺序。如果我们把颜色和方向向量的顺序搞混了,或者犯了此类错误,当我们绘制那些粒子时,我们将看到相当抽象的结果。

让我们加入一个draw()函数来结束这个类:

    fun draw(){GLES20.glDrawArrays(GLES20.GL_POINTS,0,currentParticleCount)}

我们现在就实现了一个粒子系统。这个系统可以让我们添加一定量的粒子,回收旧的粒子,并在内存中有效地定位那些彼此相邻的粒子。

添加一个粒子喷泉

有了这个粒子系统,我们现在需要某个程序,它可以为我们实际生成一些粒子,并把它们添加到粒子系统中。让我们创建一个粒子喷泉,继续创建一个名为“ParticleShooter”的新类,并加入如下代码:

class ParticleShooter(var position:Point,var direction:Vector,var color:Int) {}

我们已经给定了这个粒子发射器的位置、方向和颜色;当我们创建新的粒子时,我们只要把这些直接传递给粒子系统。让我们继续编写这个粒子发射器:

    fun addParticles(particleSystem: ParticleSystem,currentTime:Float,count:Int){for(i in 0 until count) {particleSystem.addParticle(position, color, direction, currentTime)}}

在 addParticles()中,我们传进了粒子系统、要添加多少粒子以及粒子系统的当前时间。我们现在已经有了所有的组件,只需要在Renderer类中加入一些调用把这一切组合起来。

绘制粒子系统

只需要给ParticlesRenderer加入一些代码,就能最终看到这些粒子了。让我们以下面的定义开始:

class ParticlesRenderer:Renderer {var context:Contextvar projectionMatrix = FloatArray(16)var viewMatrix = FloatArray(16)var viewProjectionMatrix = FloatArray(16)lateinit var particleProgram:ParticleShaderProgramlateinit var particleSystem: ParticleSystemlateinit var redParticleShooter: ParticleShooterlateinit var greenParticleShooter: ParticleShooterlateinit var blueParticleShooter: ParticleShootervar globalStartTime:Long = 0Lconstructor(context: Context){this.context = context}

这里定义了Android的Context和那些矩阵的标准变量,以及粒子着色器、粒子系统和三个粒子发射器的变量。我们还定义了全局启动时间的变量和一个标准构造函数。

接下来,让我们定义onSurfaceCreated()的内容:

    override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {GLES20.glClearColor(0.0f,0.0f,0.0f,0.0f)particleProgram = ParticleShaderProgram(context)particleSystem = ParticleSystem(10000)globalStartTime = System.nanoTime()val particleDirection = Vector(0f,0.5f,0f)redParticleShooter = ParticleShooter(Point(-1f,0f,0f),particleDirection, Color.rgb(255,50,5))greenParticleShooter = ParticleShooter(Point(0f,0f,0f),particleDirection, Color.rgb(25,255,25))blueParticleShooter = ParticleShooter(Point(1f,0f,0f),particleDirection, Color.rgb(5,50,255))}

我们把清屏的颜色设为黑色,初始化粒子着色器程序,并用一万个粒子的最大限量初始化一个新的粒子系统,然后,我们使用System.nanoTime()获得当前系统时间,并用它设置全局启动时间。我们想在粒子系统中使用浮点数时间,因此,当粒子系统被初始化时,当前时间将是0.0,在此时创建的粒子,其创建时间将是0.0。5秒以后,新粒子的创建时间是5.0。为此,我们可以用当前系统时间与globalStartTime的差值作为创建时间,因为System.nanoTime()返回以纳秒为单位的时间,我们只要把这个差值除以一万亿就可以转换为秒。

这个方法的下半部分建立了三个粒子喷泉。每个喷泉由一个粒子发射器表示,每个发射器都将按照particleDirection定义的方向或者沿y轴垂直向上发射它的粒子。我们把这三个喷泉按照从左到右的顺序排列,并且设置了它们的颜色:第一个是红色,第二个是绿色,第三个是蓝色。

让我们加入 onSurfaceChanged()的定义:

    override fun onSurfaceChanged(p0: GL10?, width: Int, height: Int) {GLES20.glViewport(0,0,width,height)Matrix.perspectiveM(projectionMatrix,0,45f,width.toFloat()/height.toFloat(),1f,10f)Matrix.setIdentityM(viewMatrix,0)Matrix.translateM(viewMatrix,0,0f,-1.5f,-5f)Matrix.multiplyMM(viewProjectionMatrix,0,projectionMatrix,0,viewMatrix,0)}

这是一个标准定义,它用一个常规的透视投影和视图矩阵把物体放在正确的空间中。让我们为onDrawFrame()加入如下定义完成这个渲染器:

    override fun onDrawFrame(p0: GL10?) {GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)var currentTime = (System.nanoTime() - globalStartTime) / 1000000000fredParticleShooter.addParticles(particleSystem,currentTime,5)greenParticleShooter.addParticles(particleSystem,currentTime,5)blueParticleShooter.addParticles(particleSystem,currentTime,5)particleProgram.useProgram()particleProgram.setUniforms(viewProjectionMatrix,currentTime)particleSystem.bindData(particleProgram)particleSystem.draw()}

每次一个新帧被绘制时,我们都计算当前时间并把它传给着色器。它会告诉着色器粒子自从被创建后移动了多远。我们也为每个喷泉生成了5个新的粒子,然后用粒子着色程序绘制这些粒子。

运行一下这个程序,几秒钟后,它应该看上去如下图所示。

我们现在有了一个能工作的系统,随着粒子运行时间的变化,我们会看到它由明变暗的效果,但是它看起来还是很奇怪,不是吗?当粒子向上移动时,它们不应该扩散开么?因为重力的作用,它们不应该向下回落么?接下来我们要解决这些问题及其他更多的问题。

扩散粒子

我们要做的第一件事是扩散粒子,我们也将变化每个粒子的速度,让每个粒子喷泉有更多的变化。让我们回到ParticleShooter类,将其改造为如下代码:

class ParticleShooter1(var position:Point, var direction:Vector, var color:Int, var angleVariance:Float, var speedVariance:Float) {var rotationMatrix = FloatArray(16)var directionVector = FloatArray(4)var resultVector = FloatArray(4)init {directionVector[0] =  direction.xdirectionVector[1] =  direction.ydirectionVector[2] =  direction.z}

每个发射器都将有一个角度变化量,用来控制粒子的扩散,以及一个速度变化量,用来改变每个粒子的速度。我们也有一个矩阵和两个向量,因此,我们可以使用Android的Matrix类做些数学运算。

现在,我们只需要更新addParticles(),应用角度和速度的变化量。按如下代码更新 for循环的内容:

            Matrix.setRotateEulerM(rotationMatrix,0,(Random.nextFloat() - 0.5f)*angleVariance,(Random.nextFloat() - 0.5f)*angleVariance,(Random.nextFloat() - 0.5f)*angleVariance)Matrix.multiplyMV(resultVector,0,rotationMatrix,0,directionVector,0)var speedAdjustment = 1f + Random.nextFloat() * speedVariancevar thisDirection = Vector(resultVector[0] * speedAdjustment,resultVector[1] * speedAdjustment,resultVector[2] * speedAdjustment)particleSystem.addParticle(position,color,thisDirection,currentTime)

要改变发射角度,我们使用Android的Matrix.setRotateEulerM()创建一个旋转矩阵,它会用angleVariance的一个随机量改变发射角度,其单位是度。接下来我们把这个矩阵与方向向量相乘得到一个角度稍小的旋转向量。要调整速度,我们把方向向量的每一个分量都与相同的speedVariance的随机调整值相乘。一旦完成这些,我们就通过调用particleSystem.addParticle()添加这个新的粒子。

现在需要更新ParticlesRenderer,调整新的构造函数参数。修改onSurfaceCreated(),以便粒子发射器可以按如下方式创建出来:

    override fun onSurfaceCreated(p0: GL10?, p1: EGLConfig?) {GLES20.glClearColor(0.0f,0.0f,0.0f,0.0f)val angleVarianceInDegrees = 5fval speedVariance = 1f;particleProgram = ParticleShaderProgram(context)particleSystem = ParticleSystem(10000)globalStartTime = System.nanoTime()val particleDirection = Vector(0f,0.5f,0f)redParticleShooter = ParticleShooter(Point(-1f,0f,0f),particleDirection, Color.rgb(255,50,5),angleVarianceInDegrees,speedVariance)greenParticleShooter = ParticleShooter(Point(0f,0f,0f),particleDirection, Color.rgb(25,255,25),angleVarianceInDegrees,speedVariance)blueParticleShooter = ParticleShooter(Point(1f,0f,0f),particleDirection, Color.rgb(5,50,255),angleVarianceInDegrees,speedVariance)}

我们的系统已经准备好了,每个粒子喷泉都有一个5度的发射角变化量以及一个1单位的发射速度变化量。继续运行这个应用看看我们这次看到了什么。它看起来应该如图下所示。

看起来效果好多了,现在我们需要添加重力把那些粒子拉回地面。

添加重力

任何上升到空中的东西都会掉下来因为地球有吸引力。牛顿受到苹果从树上落下的启发而发现了重力效应,他也因此而出名;如果我们也给粒子加入重力效应,效果看起来会更好。

在地球上,每个人都感受到9.8m/s²的加速度,如果我落的时间越长,它的速度就越快。通过给顶点着色器加入一个容易地在代码中实现这个效果。打开particle_vertex_shader.glsl,在v_ElapsedTime赋值语句之后加入如下代码行:

float gravityFactor = v_ElapsedTime * v_ElapsedTime / 8.0;

它通过应用重力加速度公式和粒子运行时间的平方值计算得到一个加速重力因子;我们还把它除以8以弱化这个效果。数值8是被任意使用的:可以使用其他任何数值,同样也能获得不错的效果。现在需要给当前的位置应用这个重力因子,在currentPosition赋值语句之后给顶点着色器加入如下代码:

    currentPosition.y -= gravityFactor;

让我们再次运行这个应用,看看会发生什么。它看起来应该如图下图所示所示。

现在我们看到每个向上移动的粒子都渐渐慢下来,并最终向地面回落。我们还可以让它更好看:一些较暗的粒子覆盖了那些较明亮的,这看起来有点奇怪。

累加混合技术混合粒子

当我们在OpenGL中实现各种效果时,我们不得不经常回想我们要努力仿制的那个效果。如果把这三个粒子流想象成一个喷射的烟花,就像我们在烟花表演上看到的一样,那我们应该期望这些粒子可以发光;粒子越多,它们就应该越亮。模仿这个效果的方法之一是使用累加混合技术。

让我们使能混合技术,在onSurfaceCreated()方法里为ParticlesRenderer类加入如下代码:

        GLES20.glEnable(GLES20.GL_BLEND)GLES20.glBlendFunc(GLES20.GL_ONE,GLES20.GL_ONE)

就是它!我们首先要使能混合技术,然后设置混合模式为累加混合。为了更好地理解这是怎么工作的,让我们看一下OpenGL的默认混合公式:

输出=(源因子源片段)+(目标因子目标片段)

在OpenGL里,混合技术的工作原理是把片段着色器的结果和已经在帧缓冲区中的颜色进行混合。源片段的值来自于片段着色器,目标片段的值就是已经在帧缓冲区中的值,源因子和目标因子的值是通过调用glBlendFunc()配置的。在我们刚刚添加的代码中,调用glBlendFunc()时,我们把每个因子都设为GL_ONE,它把混合公式变为如下的公式:

输出=(GL_ONE源片段)+(GL_ONE目标片段)

通过这个混合模式,来自片段着色器的片段就被累加到已经存在于屏幕上的片段了,这就是为什么它叫累加混合技术。还有其他更多的混合模式,可以在Khronos网站上在线查看。

我们的粒子现在看上去被点亮了,因为它们混合在一起了。我们还要记住OpenGL限定每一个颜色分量的值,因此,如果我们给纯绿色加上纯绿色,得到的还是纯绿色。然而,如果我们只把一小点的红色累加到那个绿色上足够多次,实际上也会偏移它的色调,最终得到黄色;再累加一点蓝色到那个黄色足够多的次数,我们就会看到白色。

考虑到OpenGL的限值特性,我们可以实现一些优美的效果。比如,在下图中,我们的红色烟花喷泉在它的最亮处实际上有点发黄,这是因为我们给它的基色上加入了一点绿色和稍少一点的蓝色。

自定义点的外形

你可能已经注意到了,这些点被渲染为小的四方块,其每条边上的像素数量都等于gl_PointSize的值。我们实际上可以使用另一个特殊的 OpenGL变量 gl_PointCoord自定义这些点的外形。对于每个点,当调用片段着色器时,我们都会得到一个二维的gl_PointCoord坐标空间,在每个轴上,其分量的范围都是从0到1,其取值依赖于点上的哪个片段当前正在被渲染。

要知道这是怎么工作的,我们首先要使用gl_PointCoord把片段绘制为圆,而不是方形。我们怎么做呢?渲染每个点时,相对于gl_PointCoord上的每个轴来说,其片段的位置范围都是从0到1,因此,我们把点的圆心放在(0.5,0.5),其每条边都有0.5单位的空间。换句话说,点的半径是0.5。要绘制一个圆,我们所要做的就是只绘制那些位于半径内的片段。

首先让我们调高点的大小,以便使它更容易被看到。将 particle_vertex_shader.glsl中的gl_PointSize更新为如下值:

precision mediump float;
varying vec3 v_Color;
varying float v_ElapsedTime;void main(){float xDistance = 0.5 - gl_PointCoord.x;float yDistance = 0.5 - gl_PointCoord.y;float distanceFromCenter = sqrt(xDistance*xDistance + yDistance*yDistance);if(distanceFromCenter > 0.5){discard;} else {gl_FragColor = vec4(v_Color / v_ElapsedTime,1.0);}
}

用这种方法把一个点绘制为圆,其开销有点大,但是它能工作。它的工作原理是:对于每个片段,我们使用勾股定理计算其与圆心的距离。如果那个距离大于0.5单位的半径,那么当前的片段就不是圆的一部分,我们还使用了特殊的关键词“discard”告诉OpenGL丢掉这个片段;否则,我们还像以前一样绘制这个片段。

让我们再次运行这个应用;它看上去应该如下图所示(去掉混合可能会更容易观察)。

把每个点绘制为一个精灵

我们刚刚学过的技术是可行的,但是有时候纹理更有效。使用相同的gl_PointCoord和纹理,实际上可以把每个点绘制为一个点精灵。对于每个粒子,我们将改动粒子着色器,让它使用下图所示的纹理。

要实现本项目中的纹理,首先更新片段着色器,加入如下uniform:

uniform sampler2D u_TextureUnit;

去掉我们在前一节加入的画圆的逻辑,按如下代码更新 gl_FragColor的赋值语句:

gl_FragColor = vec4(v_Color / v_ElapsedTime,1.0) * texture2D(u_TextureUnit , gl_PointCoord);

它会使用gl_PointCoord作为纹理坐标在每个点上绘制一个纹理。纹理的颜色会与点的颜色相乘,这样就可以用与以前一样的方式将这些点染上颜色。

我们现在需要给ParticleShaderProgram加入新的uniform。首先加入如下成员变量:

    var uTextureUnitLocation = 0

在构造函数的结尾处加入如下代码:

        uTextureUnitLocation = findUniformLocationByName(U_TEXTURE_UNIT)

我们需要把 setUniforms()的签名更新为如下代码:

    fun setUniforms(matrix:FloatArray,elapsedTime:Float,textureId:Int){

我们也需要在 setUniforms()的结尾处加人如下代码:

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId)GLES20.glUniform1i(uTextureUnitLocation,0)

我们只需要加载这个新的纹理,就准备好运行这个应用程序了。打开ParticlesRenderer,
并加人下面的新成员变量:

var texture:Int = 0

在 onSurfaceCreated()的结尾处加入如下代码:

        texture = TextureHelper.loadTexture(context,R.drawable.particle_texture)

现在我们可以在onDrawFrame()中更新调用particleProgram.setUniforms()了,如下代码所示:

        particleProgram.setUniforms(viewProjectionMatrix,currentTime,texture)

最后运行效果如下图。

小结

我们已经讲述了粒子系统的一些基本内容,但事实上我们只触及了一些皮毛而已。粒子可以实现很多效果,包括逼真的火焰、雨和雪等。我们学习了如何用OpenGL的GL_POINTS模式绘制粒子,我们还进一步讨论了如何用gl_PointCoord及discard关键词自定义点的形状;也可以使用纹理更进一步定制点的外形。有了这些能力,我们可以实现一些相当优美的效果。

相关文章:

Android OpenGL粒子特效

在本篇&#xff0c;我们将开启一个新的项目&#xff0c;探索粒子的世界。粒子是一种基本的图形元素&#xff0c;它们通常被表示为一组点。通过巧妙地组合一些基础的物理效果&#xff0c;我们能够创造出许多令人惊叹的视觉效果。想象一下&#xff0c;我们可以模拟一个水滴从喷泉…...

5 -《本地部署开源大模型》在Ubuntu 22.04系统下ChatGLM3-6B高效微调实战

在Ubuntu 22.04系统下ChatGLM3-6B高效微调实战 无论是在单机单卡&#xff08;一台机器上只有一块GPU&#xff09;还是单机多卡&#xff08;一台机器上有多块GPU&#xff09;的硬件配置上启动ChatGLM3-6B模型&#xff0c;其前置环境配置和项目文件是相同的。如果大家对配置过程还…...

dpkg:错误:另外一个进程已经为dpkg前端锁加锁

一、 问题描述 在新装ubuntu系统时&#xff0c;我们常常会遇见dpkg的错误&#xff0c;dpkg:错误:另外一个进程已经为dpkg前端锁加锁&#xff0c;如下图。 二、问题解决 方法一 先执行sudo rm /var/lib/dpkg/lock-frontend然后再继续安装软件包&#xff0c;如果出现问题dpkg:…...

基于SSM服装定制系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;服装类型管理&#xff0c;服装信息管理&#xff0c;服装定制管理&#xff0c;留言反馈&#xff0c;系统管理 前台账号功能包括&#xff1a;系统首页&#xff0c;个人中心&#xf…...

RK3588开发笔记-usb3.0 xhci-hcd控制器挂死问题解决

目录 前言 一、问题现象 二、问题分析 三、问题排查 总结 前言 在使用 RK3588 芯片进行开发的过程中,我遇到了 USB 3.0 xHCI-HCD 控制器外接5G通讯模块偶尔挂死的问题。这个问题导致 USB 设备失去响应,且不能恢复,需要重启整个系统才能恢复使用,针对该问题进行大量测试以…...

深入解析TCP/IP协议:网络通信的基石

1. 引言 TCP/IP 协议是现代计算机网络的核心&#xff0c;它为互联网上的设备提供了通信的基础。在网络通信中&#xff0c;TCP/IP 协议栈是无处不在的&#xff0c;无论是个人设备的浏览器请求&#xff0c;还是大型分布式系统的内部通信&#xff0c;都依赖于它的稳定、高效与可靠…...

基于微信小程序的汽车预约维修系统(lw+演示+源码+运行)

基于微信小程序的汽车预约维修系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了基于微信小程序的汽车预约维修系统的开发全过程。通过分析基于微信小程序的汽车预约维修系统管理的不足&#xff0c;创建了…...

wifi、热点密码破解 - python

乐子脚本&#xff0c;有点小慢&#xff0c;试过多线程&#xff0c;系统 wifi 连接太慢了&#xff0c;需要时间确认&#xff0c;多线程的话系统根本反应不过来。 也就可以试试破解别人的热点&#xff0c;一般都是 123456 这样的傻鸟口令 # coding:utf-8 import pywifi from pyw…...

bean的实例化2024年10月17日

跟不上为基础 1.你的java学习路线 2. 3.课程 注解的装配 contoller调用service用的是注解装配...

告别ELK,APO提供基于ClickHouse开箱即用的高效日志方案——APO 0.6.0发布

ELK一直是日志领域的主流产品&#xff0c;但是ElasticSearch的成本很高&#xff0c;查询效果随着数据量的增加越来越慢。业界已经有很多公司&#xff0c;比如滴滴、B站、Uber、Cloudflare都已经使用ClickHose作为ElasticSearch的替代品&#xff0c;都取得了不错的效果&#xff…...

Excel使用技巧:定位Ctrl+G +公式+原位填充 Ctrl+Enter快速填充数据(处理合并单元格)

Excel的正确用法&#xff1a; Excel是个数据库&#xff0c;不要随意合并单元格。 数据输入的时候一定要按照行列输入&#xff0c;中间不要留空&#xff0c;不然就没有关联。 定位CtrlG 公式原位填充 CtrlEnter快速填充数据 如果把合并的单元格 取消合并&#xff0c;只有第一…...

JAVA学习-练习试用Java实现“成绩归类”

问题&#xff1a; 编写程序&#xff0c;输入一批学生的成绩&#xff0c;遇0或负数则输入结束&#xff0c;要求统计并输出优秀&#xff08;大于85&#xff09;、通过&#xff08;60&#xff5e;84&#xff09;和不及格&#xff08;小于60&#xff09;的学生人数。 示例 &#x…...

【Hive】7-拉链表的设计与实现

拉链表的设计与实现 数据同步问题 背景 例如&#xff1a;MySQL中有一张用户表: tb_user&#xff0c;每个用户注册完成以后&#xff0c;就会在用户表中新增该用户的信息&#xff0c;记录该用户的id、手机号码、用户名、性别、地址等信息。 每天都会有用户注册&#xff0c;产生…...

Maxwell 底层原理 详解

Maxwell 是一个 MySQL 数据库的增量数据捕获&#xff08;CDC, Change Data Capture&#xff09;工具&#xff0c;它通过读取 MySQL 的 binlog&#xff08;Binary Log&#xff09;来捕获数据变化&#xff0c;并将这些变化实时地发送到如 Kafka、Kinesis、RabbitMQ 或其他输出端。…...

使用短效IP池的优势是什么?

短效IP池作为代理IP服务中一种独特的资源管理方式&#xff0c;其应用已经在数据采集、市场分析和网络安全等多个领域中展示出强大的功能。尽管“短效”听起来似乎意味着某种限制&#xff0c;然而在某些特定的应用场景下&#xff0c;短效IP池却提供了无可比拟的优势。本文将详细…...

zynq烧写程序到flash后不运行

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…...

JMeter如何设置HTTP代理服务器?

1、 2、添加线程组 3、设置HTTP代理服务器&#xff0c;目标控制器选择“测试计划>线程组” 过滤掉不需要的信息 4、设置电脑手动代理 5、点击启动&#xff0c;在浏览器操作就可以了...

React面试题笔记(一)

一、react基础面试题 1.react中keys的作用是什么? key是是用于追踪哪些列表被修改&#xff0c;被添加或者被移除的辅助标识。 在开发过程中&#xff0c;我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是…...

3.Java入门笔记--基础语法

1.字面量 概念&#xff1a;计算机用来处理数据的&#xff0c;字面量就是告诉程序员数据在程序中的书写格式 常用数据&#xff1a;整数&#xff0c;小数直接写&#xff1b;字符单引号&#xff08;A&#xff09;且只能放一个字符&#xff1b;字符串双引号&#xff08;"Hel…...

关于SOCKS协议的常见误区有哪些?

代理协议在设备与代理服务器之间的数据交换中起到了关键作用。在这方面&#xff0c;SOCKS代理协议是常见的选择之一&#xff0c;被广泛应用于下载、传输和上传网络数据的场景。然而&#xff0c;关于SOCKS代理协议存在一些常见的误解&#xff0c;让我们来逐一了解。 一、使用SO…...

无极低码课程【redis windows下服务注册密码修改】

下载Windows版本的Redis linux环境 (自行下载) 1.打开官网https://redis.io/downloads/ windows环境 1.打开github https://github.com/microsoftarchive/redis/releases 然后选择你喜欢的版本zip或msi下载 2.这里下载zip版,解压后后,打开安装目录 3.双击redis-server…...

多ip访问多网站

1,关闭防火墙和安全软件 [rootlocalhost ~]# systemctl stop firewalld.service [rootlocalhost ~]# setenforce 02,挂载点&#xff0c;下载nginx [rootlocalhost ~]# mount /dev/sr0 /mnt [rootlocalhost ~]# dnf install nginx -y 3,一个虚拟机增加多个ip地址 [rootloc…...

Pytest参数详解 — 基于命令行模式!

1、--collect-only 查看在给定的配置下哪些测试用例会被执行 2、-k 使用表达式来指定希望运行的测试用例。如果测试名是唯一的或者多个测试名的前缀或者后缀相同&#xff0c;可以使用表达式来快速定位&#xff0c;例如&#xff1a; 命令行-k参数.png 3、-m 标记&#xff08;…...

指针——函数指针数组

&#xff08;一&#xff09;前文回顾 1、前篇代码分析 void(*signal(int , void(*)(int)))(int) ; 那么这串代码究竟是什么呢&#xff1f; 别慌&#xff0c;让我们来一步一步拆解&#xff0c;首先我们通过之前的学习&#xff0c;已经明白了什么是函数指针&#xff08;如果有…...

MySQL中的增查操作:探索数据的奥秘,开启数据之门

本节&#xff0c;我们继续深入了解MySQL&#xff0c;本章所讲的基础操作&#xff0c;针对的是表的增删查改&#xff01; 一、Create 新增 1.1、语法 INSERT [INTO] table_name[(column [, column] ...)] VALUES(value_list) [, (value_list)] ... value_list: value, [, va…...

oracle_查询建表语句

查询建表语句 SELECTdbms_metadata.get_ddl ( TABLE, <table_name> ) FROMdualdbms_metadata.get_ddl&#xff1a;是Oracle提供的一个函数&#xff0c;用于获取数据库对象的DDL语句&#xff0c;它允许你查看或导出数据库对象的创建脚本‘TABLE’&#xff1a; 是这个函数…...

004-按照指定功能模块名称分组

按照指定功能模块名称分组 一、说明1.现在有一个需求&#xff1a;2.具体做法 二、代码案例三、效果展示 一、说明 1.现在有一个需求&#xff1a; 需要把一个功能模块的几个功能点放在同一个文档目录下&#xff0c;这几个功能点分布在不同的 Controller 2.具体做法 需要把他…...

ChatGPT写作助手:论文写作必备提示词一览

学境思源&#xff0c;一键生成论文初稿&#xff1a; AcademicIdeas - 学境思源AI论文写作 随着人工智能技术的发展&#xff0c;ChatGPT在学术写作领域的应用越来越广泛。它不仅能够帮助撰写论文&#xff0c;还可以通过不同的提示词完成构思、文献综述、数据分析、润色等任务&a…...

大数据开发电脑千元配置清单

大数据开发电脑配置清单 电脑型号HUANANZHI 台式电脑操作系统Windows 11 专业版 64位&#xff08;Version 23H2 / DirectX 12&#xff09;处理器英特尔 Xeon(至强) E5-2673 v3 2.40GHz主板HUANANZHI X99-P4T&#xff08;P55 芯片组&#xff09;显卡NVIDIA GeForce GT 610 ( 2…...

VP9官方手册-帧内预测

8.5.1 intra prediction process...