当前位置: 首页 > 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…...

Xshell远程连接Kali(默认 | 私钥)Note版

前言:xshell远程连接&#xff0c;私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...

Zustand 状态管理库:极简而强大的解决方案

Zustand 是一个轻量级、快速和可扩展的状态管理库&#xff0c;特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

visual studio 2022更改主题为深色

visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中&#xff0c;选择 环境 -> 常规 &#xff0c;将其中的颜色主题改成深色 点击确定&#xff0c;更改完成...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

Oracle11g安装包

Oracle 11g安装包 适用于windows系统&#xff0c;64位 下载路径 oracle 11g 安装包...

算法打卡第18天

从中序与后序遍历序列构造二叉树 (力扣106题) 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7…...