Android OpenGL ES绘制圆形全攻略:从三角形扇到着色器工程实战
简介一份面向 Android 图形开发学习者的 OpenGL ES 圆形绘制示例工程。项目通过 OpenGL 实现了圆饼填充、圆环描边与点状圆圈的绘制覆盖从顶点数据生成、着色器编写到渲染状态设置的完整流程。资源包共 567 个文件以 xml 布局与资源、gradle 构建脚本、glsl 着色器源码、java 程序逻辑为主并附带 dex、apk 等可直接运行或安装的产物整体约 9.68MB。压缩包内目录结构清晰适合初学者直接导入工程查看源码并对照运行效果。当前已有 794 人学习下载。通过阅读代码可重点理解圆形顶点坐标计算、索引缓冲组织方式以及如何在 GLES2 中通过 glDrawArrays 或 glDrawElements 绘制不同形态的圆对于希望掌握 Android 平台基础图形渲染流程的开发者是一份简洁且可动手调试的参考资料。1. 绘制圆形Android 上打开 OpenGL ES 的第一道真实门槛在 Android 上做图形开发很多人的第一个自测题就是绘制圆形。别觉得它简单OpenGL ES 本身没有“圆形”这个图元你实际要做的是用三角形把圆“拼”出来再交给渲染管线填充。一个圆里同时藏着顶点生成、坐标变换、缓冲管理和着色器连接这几件事哪一环没对画面要么是椭圆要么缺一角。这份资源把 OpenGL ES 绘制圆形的完整链路拆开讲新手能按步骤跑出第一个纯色圆熟手也能省下调参和排错的时间直接抄一套可用的工程模板。2. 为什么 OpenGL ES 没有“圆形”这个图元三角剖分是绕不开的起点OpenGL ES 2.0 的可绘制图元只有点、线、三角形三种抽象圆形本质上是“三角形拼出来的多边形”。这点很多人刚接触时不适应我们平时画圆用的是 canvas 提供的 arc 接口但到了 GL 世界没有哪个 API 会替你生成圆。要做的是先把圆的几何数据算出来再交给 GPU 去填充。这一章先把三角剖分的思路讲透因为后续所有调参、报错、视觉问题都根植于“怎么组织这些三角形顶点”这件事上。2.1 从图元限制到几何逼近为什么只能靠三角形“装”出圆先看一个最直接的问题能不能用GL_POINTS画圆答案是否定的。ES 2.0 里的点虽然可以设置gl_PointSize点的大小范围在不同 GPU 上有严格上限而且点本身是方形光栅化撑不起大半径圆也不适合复杂着色。线图元同样不行GL_LINES只能描边没法填充满内部更没法带纹理。三角形就成了唯一合理的填充图元。用三角形逼近圆的思路很朴素把圆看成内接正多边形边数越多多边形越接近圆。这个思想叫几何逼近在实际渲染中正 32 边形和正 64 边形在普通手机屏上几乎看不出差异所以没人真的去追求“数学意义上的完美圆”而是追求“肉眼分辨率下的圆”。我遇到不少第一次做这个的开发者会把所有三角形顶点逐个手写进数组。其实不用标准做法是从圆心和圆周的角度步进生成。选型上常见的三角形组织方式有三种三角形列表、三角形带、三角形扇。三者各有适用场景对比一下组织方式顶点数量绘制模式适合场景GL_TRIANGLES顶点多每个三角形独立3n 个顶点不规则多边形、网格模型GL_TRIANGLE_STRIP共享边节省顶点n2带状结构、地形条带GL_TRIANGLE_FAN以首点为公共顶点n2凸多边形、圆形扇形圆形是个凸多边形而且所有三角形共享圆心这个公共顶点用三角形扇是顶点数最少、最自然的选择。这也是 OpenGL ES 绘制圆形资源包里的主流实现方案。2.2 三角形扇的顶点结构中心点为什么要单独保留一份三角形扇的规则是第一个顶点作为扇形的公共顶点之后每两个顶点和第一个顶点组成一个三角形。结构上就是“圆心 圆周上一圈顶点”。顶点数组的组织方式非常关键下面这段代码是整套工程里最核心的生成函数// 生成三角形扇顶点中心点 圆周上 segments1 个点 // 返回的数组长度为 (segments 2) * 2 private float[] createCircleVertices(float cx, float cy, float radius, int segments) { float[] vertices new float[(segments 2) * 2]; int index 0; // 先放圆心这个点会被所有三角形共享 vertices[index] cx; vertices[index] cy; // 圆周上的点角度从 0 到 2π共 segments1 个点 for (int i 0; i segments; i) { double angle 2.0 * Math.PI * i / segments; vertices[index] cx radius * (float) Math.cos(angle); vertices[index] cy radius * (float) Math.sin(angle); } return vertices; }这里有个容易看漏的细节循环写的是i segments而不是i segments。因为扇形闭合时需要让最后一个点和第一个圆周点重合这样最后一个三角形才有足够的边长不会在闭合处留一条细缝。再解释一下数组长度(segments 2) * 2里的2代表圆心这一个点加上闭合处重复的那个圆周点乘以 2 是因为每个顶点只有 x、y 两个分量。假设 segments 是 64那么数组一共是 132 个 float渲染时传入的顶点数是 66稍后glDrawArrays里会用到这个数字。2.3 分段数多少才够视觉圆滑、锯齿和终端功耗的三角关系分段数是最常被乱调的参数默认写死一个 100 就完事但这在真机上很容易出现“又糊又卡”的观感。先看数学误差。当半径是 R 像素、分段数是 n 时内接多边形到圆弧的最大距离约是R * (1 - cos(π / n))。我实际测过几个典型值现在这个数据放在参数表里很直观分段数 n半径 50px 时最大凹陷半径 200px 时最大凹陷肉眼观感16约 1.9px约 7.6px有明显棱角32约 0.5px约 1.9px近距离能辨出棱64约 0.12px约 0.48px正常距离看是圆128约 0.03px约 0.12px基本无感分段数并不是越大越好。顶点数组一次生成后GPU 每帧顶点处理量都会随分段数线性上涨在低端机上同时画几十个圆时帧率下降肉眼可见。我一般不会写死分段数而是让分段数跟随半径动态走经验公式是// 半径越大分段越多但不能无限制 int segments (int) (radius * Math.PI / 3); segments Math.max(12, Math.min(segments, 128));这个式子的思路是让每段圆弧长度控制在 3 像素左右这样小圆不会浪费顶点大圆也不会糊边。上限压到 128因为超过 128 之后视觉提升微乎其微而内存和带宽成本一直在加。2.4 归一化坐标还是像素坐标生成顶点前先决定坐标系生成顶点前必须想清楚一个问题圆心的 cx、cy 用什么坐标系。如果直接用归一化设备坐标圆的 x 范围在 [-1, 1]y 范围在 [-1, 1]写起来会很绕比如屏幕右侧的圆心要写成 0.7半径要写成 0.2不好维护。如果直接用屏幕像素坐标圆心就是(screenWidth / 2f, screenHeight / 2f)半径 150 就是 150很直观但需要靠投影矩阵把像素坐标换算到 NDC。这套资源包推荐像素坐标方案。原因是后续项目里大概率还要处理触摸交互触摸回调拿到的坐标本来就是屏幕像素保持一致能少做一层转换踩坑概率小得多。3. 最小可运行工程搭建着色器、VBO、GLSurfaceView 三条线怎么接从零搭一个能跑出圆形的 Android 工程需要依次接通三样东西GPU 着色器程序、CPU 侧顶点缓冲、以及承载渲染的 GLSurfaceView。很多人失败是因为这三条线各自能跑通但接到一起时衔接处出了问题比如着色器里属性名对不上、缓冲 offset 传错、生命周期没处理。这一章直接给出一份能跑通的最小实现。3.1 顶点着色器和片段着色器怎么写MVP 变换几何颜色留在片元OpenGL ES 2.0 的着色器必须成对出现。顶点着色器负责变换顶点坐标片段着色器负责填充颜色。这段 GLSL 是整个工程里最短也最稳定的部分// 顶点着色器把二维圆形顶点送入 MVP 矩阵 uniform mat4 u_MvpMatrix; attribute vec4 a_Position; void main() { gl_Position u_MvpMatrix * a_Position; }// 片段着色器输出单一颜色 precision mediump float; uniform vec4 u_Color; void main() { gl_FragColor u_Color; }顶点着色器里u_MvpMatrix是投影矩阵和模型矩阵的乘积这个矩阵把像素坐标转换成 GPU 需要的归一化设备坐标。a_Position是从顶点缓冲里取出来的 x、y 数据我在这里声明为vec4是因为 GLSL 的 attribute 从缓冲里取分量时即使缓冲只给了 2 个分量也能自动填充 z0、w1。片段着色器里precision mediump float是 ES 的精度声明写上它不写 float 精度时会报编译错误这是新手常见的坑。u_Color是个 uniform 变量之后动态改变颜色时不用重新编译着色器只需要更新这个值。3.2 接线 GLSurfaceView生命周期、渲染模式与 GL 上下文丢失GLSurfaceView 是 Android 系统提供的 GL 渲染容器它自己管理 EGL 上下文和渲染线程。标准初始化代码是这样// 在 Activity 的 onCreate 中初始化 GLSurfaceView glSurfaceView new GLSurfaceView(this); glSurfaceView.setEGLContextClientVersion(2); glSurfaceView.setRenderer(new CircleRenderer()); glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); setContentView(glSurfaceView);setEGLContextClientVersion(2)告诉系统创建 OpenGL ES 2.0 上下文版本不匹配时会直接抛异常真机上多见于 GLES 2.0 不支持的模拟器。setRenderer必须传入一个实现GLSurfaceView.Renderer接口的实例这个接口包含onSurfaceCreated、onSurfaceChanged、onDrawFrame三个回调。setRenderMode(RENDERMODE_WHEN_DIRTY)是一个值得强调的优化点它让 GL 只在调用了requestRender()时重绘一帧适合静态画面。如果做的是动画、游戏或者圆形要跟随手指移动就用RENDERMODE_CONTINUOUSLY。我习惯在工程里默认用WHEN_DIRTY需要动画时再加一个切换开关省电效果在低端机上立竿见影。Activity 的onPause()和onResume()必须转发给 GLSurfaceView否则切后台再回来时 GL 上下文丢失纹理和 VBO 全部作废画面会黑屏或闪退。3.3 顶点缓冲和绘制调用Buffer 为什么要留 directByteBufferCPU 算出来的顶点数组不能直接传给 GPU需要先包装成FloatBuffer而且必须是直接字节缓冲这一步很多新手会忘。标准写法如下// CPU 顶点数组 - FloatBuffer FloatBuffer vertexBuffer ByteBuffer.allocateDirect(vertexArray.length * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); vertexBuffer.put(vertexArray); vertexBuffer.position(0);allocateDirect分配的是 JVM 堆外的直接内存这样 GPU 驱动才能直接读取不会发生堆内数组的拷贝或 GC 回收问题。* 4是因为每个 float 占 4 字节。order(ByteOrder.nativeOrder())必须和宿主机的字节序一致否则高低位反过来画出来的坐标会乱。接下来把 FloatBuffer 上传到 GPU 并绘制// 生成一个 VBO 并上传顶点数据 int[] vboId new int[1]; GLES20.glGenBuffers(1, vboId, 0); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId[0]); GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexBuffer.capacity() * 4, vertexBuffer, GLES20.GL_STATIC_DRAW); // 设置顶点属性指针告诉 GPU 数据怎么解读 int positionHandle GLES20.glGetAttribLocation(program, a_Position); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, 0); // 绘制66 个顶点组成一个三角形扇 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount);glBufferData的第三个参数就是刚才的 FloatBuffer上传完成后 buffer 可以复用。glVertexAttribPointer的参数依次是属性位置、每顶点分量数、数据类型、是否归一化、步长、偏移量这里偏移量传0表示从缓冲开头读因为整个 VBO 只有坐标数据。需要注意如果未来在同一个 VBO 内存多个几何体步长和偏移量就不是 0 了届时偏移量必须传入“字节偏移”不是顶点偏移。这个细节我在第 5 章还会单独讲因为真写多几何体时会在这翻车。3.4 渲染管线里的 Viewport 与清屏为什么设置顺序会影响画面最后补上 Renderer 类里最容易忽略的两个参数视口和清屏颜色。它们看似不参与圆形的几何计算但设置不当会导致画面错位或残留上一帧残影。Override public void onSurfaceChanged(GL10 gl, int width, int height) { // 视口必须和 Surface 实际尺寸一致否则画面被拉伸 GLES20.glViewport(0, 0, width, height); // 后续投影矩阵的宽高比也基于这个尺寸计算 updateProjection(width, height); } Override public void onSurfaceCreated(GL10 gl, EGLConfig config) { GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // 编译着色器、生成 VBO 的工作在这里做不要放 onDrawFrame } Override public void onDrawFrame(GL10 gl) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 绑定计算好的 MVP 矩阵并执行绘制 GLES20.glUniformMatrix4fv(mvpHandle, 1, false, mvpMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount); }glViewport决定了 OpenGL 把画面画到 Surface 的哪个区域。屏幕旋转时 width 和 height 互换如果只设了一次横竖屏切换后画面就会被拉伸圆会变椭圆这个现象会在第 5 章详细排查。glClearColor设置的是清屏颜色建议在onSurfaceCreated里初始化。有的资源会把新增顶点、编译着色器的动作全塞进onDrawFrame这是低帧率的罪魁祸首之一正确做法是把这些一次性工作都放到创建回调里。4. 参数怎么调才不出戏半径、圆心与投影矩阵怎样改才不会让圆变形画出一个圆的代码在手接下来读者多半会立刻开始改参数换个颜色、改个半径、把圆移到屏幕别的位置。这一章专门讲参数调节时最容易出错的地方也是把工程从“能画”推向“能用”的关键一步。4.1 投影矩阵选择正交投影和透视投影怎么选绘制一个 2D 圆形选择正交投影是合理的如果目标是一个躺在三维空间里的圆才考虑透视投影。正交投影的特点是近处和远处的物体同样大小不会产生近大远小的透视变形所以做 UI、小游戏里的圆形按钮时都用它。Android 的Matrix工具类提供了现成的正交投影接口// 以像素坐标直接构建正交投影矩阵 // left, right, bottom, top 对应屏幕的四个边界 Matrix.orthoM(mProjMatrix, 0, 0f, screenWidth, 0f, screenHeight, -1f, 1f);这样设置之后圆心(screenWidth / 2f, screenHeight / 2f)就会出现在屏幕几何中心半径直接填 100 就是 100 像素。near 和 far 填-1和1是因为二维场景里 z 坐标始终为 0足够覆盖。有个细节很容易翻车orthoM处理的是窗口坐标它的 y 轴方向是自下而上。屏幕左上角坐标是(0, screenHeight)不是(0, 0)。如果一开始的目标是让圆形显示在屏幕左上角写(0, 0)会让圆跑到左下角去。这个方向性问题我下面会专门给出一节因为几乎每个新手都会踩。4.2 圆心坐标的三种表达方式在 OpenGL ES 工程里定义圆心有三种常见方式它们的写作方式完全不同接混了就会画错位置。第一种是直接改顶点数据生成圆形顶点时就把 cx、cy 填成目标位置。这种方式理论上任选但如果动态移动圆心每一帧都要重新生成整个顶点数组性能很差。第二种是在顶点着色器里用 uniform 表达位置不在顶点里写死中心坐标。例如顶点数组始终以(0, 0)为圆心生成然后通过一个u_Center和u_Radius做偏移// 顶点着色器 uniform vec2 u_Center; uniform float u_Radius; attribute vec2 a_PositionFromCenter; void main() { vec2 pos u_Center a_PositionFromCenter * u_Radius; gl_Position u_MvpMatrix * vec4(pos, 0.0, 1.0); }第三种是把“圆心偏移”放到模型矩阵里配合Matrix.translateM。这种方式最符合引擎思维资源和 UI 层代码解耦我目前最常用的是它。// 生成一次单位圆顶点之后靠矩阵控制位置和大小 Matrix.setIdentityM(mModelMatrix, 0); Matrix.translateM(mModelMatrix, 0, centerX, centerY, 0f); Matrix.scaleM(mModelMatrix, 0, radius, radius, 1f); Matrix.multiplyMM(mMvpMatrix, 0, mProjMatrix, 0, mModelMatrix, 0);三种方式对比一下表达方式优点缺点适用场景改顶点数组直观动态移动成本高静态固定圆着色器 uniform灵活顶点只需一份要写更多 uniform 逻辑大量相似圆形模型矩阵标准图形学做法需要理解矩阵乘法复合变换、拖动缩放如果只是画一个静态圆第一种最快如果后面要做触摸拖动、缩放动画建议直接用第三种图形学里这叫“模型矩阵变换”属于通用知识学会了整个 OpenGL ES 都受益。4.3 半径和颜色动态更新尽量走 uniform不要每次重建 VBO很多初学者第一次做交互时会在手指滑动时重新生成顶点数组再重新调用glBufferData。这在单个圆形时还能跑一旦圆的数量增多或半径变大就会出现明显的掉帧和 GC 卡顿。正确的做法是让半径参与矩阵缩放或着色器 uniform 处理。比如把刚才的单位圆顶点生成函数改为固定半径 1然后用scaleM去控制实际大小。这样顶点缓冲只需要创建一次每帧只改几个矩阵元素开销可以忽略。颜色也是一样的道理用glUniform4f更新u_Color// 动态设置圆的颜色避免重新编译着色器 GLES20.glUniform4f(colorHandle, r, g, b, alpha);这套工程里我把“创建缓冲”和“更新参数”两件事拆得很开onSurfaceCreated里生成单位圆顶点和编译着色器绘制时只管绑定矩阵、设置 uniform 和调用glDrawArrays。这个习惯帮我在后续项目里省掉了很多性能排查的时间。4.4 闭合边界的细节最后一个顶点要不要和第一个顶点重合回到第 2 章的顶点生成函数。循环条件写成i segments还是i segments很多人都混过。如果写成生成的圆周点会停在最后一个弧段之前扇形闭合时从圆心到末尾点的连线会横跨缺口画面里形成一个难看的楔形缝。这个缝在 segments 较小时非常明显在 segments 大到 128 时可能只有一两个像素在有锯齿的低分辨率屏幕上几乎看不见。但等到做带纹理的圆环时这条缝会被拉伸成一条亮线非常显眼。解决方式是循环里加一个等于号让最后一个顶点和第一个圆周点坐标完全一致。有的人会担心重复点导致 GPU 重复处理其实三角形扇中这种闭合是标准做法开销极小。还有个细节浮点精度会导致闭合点的计算值和首点有微小差异这个差异在数学上无伤大雅因为三角形扇最后一个三角形的顶点靠得很近不会造成肉眼可见缝隙。但如果为了“省一个顶点”去掉闭合点那才是真正给自己挖坑。5. 避坑与常见问题圆画得不对时按顺序排查这六个现象这一章直接从真机开发里那些反复出现的报错和画面异常入手按“现象 → 原因 → 解决”的方式拆。我尽量保证每一条都是自己或身边人真实踩过的不写那种“看了一眼就知道答案”的鸡汤总结。如果你画出来的圆不正常按下面的顺序排查一段基本能覆盖九成问题。5.1 圆变成椭圆了投影矩阵和视口尺寸没有同步更新现象同一个 gl 工程竖屏的时候圆很正常横屏之后圆变成了椭圆半径偏移方向也不对。原因glViewport和投影矩阵里的宽高比没有在屏幕尺寸变化时同步更新。很多人只在onSurfaceCreated里调用了一次orthoM但屏幕旋转会触发onSurfaceChanged里面计算的屏幕宽度和高度已经变了投影矩阵还停留在旧值。解决把投影矩阵的计算放到onSurfaceChanged中拿到最新width和height后重新调用更新逻辑。注意在使用Matrix.orthoM时要基于新的宽高重新生成矩阵这个矩阵是每帧绘制用的不要缓存到全局变量后忘了刷新。这一条还有一个隐藏变体有些资源为了让适配简单会把投影矩阵的宽高笔写成固定 4:3 或者 16:9但在全面屏手机上屏幕宽高比不同圆就会变成扁的。5.2 圆少了一半或只出现扇形三角形扇的顶点环绕方向不对现象绘制结果像是被切掉一块有时候是整个圆只剩一个直角三角形有时候是扇形只有 30 度。原因三角形扇的顶点默认要求按逆时针方向排列如果循环里判断角度的方式写成了顺时针生成的三角形会被 GPU 剔除掉。还有一些工程开启了glEnable(GL_CULL_FACE)之后环绕方向反了整个三角形都会被丢弃画面直接空掉。解决把角度步进方向改为逆时针也就是角度从 0 递增到 2π。如果开启背面剔除还需要确保在onDrawFrame里正确设置glFrontFace或关闭glCullFace。调试时可以临时关掉剔除确认问题出在方向还是顶点生成逻辑。角度表达式里还有一个变化cos(angle)和sin(angle)写反也会出现奇怪的扇形残缺因为那样生成的点顺序错乱形变后的三角形互相重叠。5.3 圆心怎么都对不上屏幕位置y 轴方向搞反了现象想放在屏幕左上角的圆出现在左下角想在右下角的圆出现在右上角。原因OpenGL 的默认坐标系是 y 轴向上而 Android UI 坐标系是 y 轴向下。当直接用屏幕坐标填orthoM时屏幕左上角坐标在 GL 里其实是 y 值为屏幕高度的位置。解决投影矩阵构建时让坐标范围从上往下或者把顶点生成阶段的 y 符号换算一遍。我常用的是在业务层单独封装一个坐标转换函数应用层传入 UI 使用的坐标系底层统一转换成 GL 坐标系。这样 UI 层和 GL 层各自清楚不会在代码里到处散落-y这种硬编码。另一个相关问题是glUniformMatrix4fv传矩阵时是否要转置。Android 的Matrix工具类默认是列主序直接传false即可传true会让矩阵完全转置圆心和半径位置都会乱套。5.4 换了背景或增加纹理后闪退着色器属性绑定顺序不一致现象单独跑纯色圆没问题但在工程里加入了纹理采样、或换了片段着色器代码之后程序启动后随机闪退logcat 里报GL_INVALID_OPERATION。原因新增纹理后片段着色器里多了一个sampler2D和对应的uniform采样器但程序里没有正确设置纹理单元。或者新增了顶点属性之后glVertexAttribPointer的绑定顺序和设置顺序不一致导致某个 attribute 被当成另一个用。解决每次修改着色器代码后强制走一遍glGetError()检查定位到具体报错指令。设置纹理单元时明确调用glActiveTexture(GL_TEXTURE0)再绑定纹理。属性绑定的正确做法是在链接程序后立即调用glGetAttribLocation拿到统一的 location再用这个 location 去设置指针不要手写死数字。还有一次我遇到的是onSurfaceCreated里拉了新的线程去加载纹理GL 上下文是线程敏感的从别的线程调用 GL 接口会直接闪退。如果资源加载需要异步应该先加载图片数据生成 Bitmap再回到 GL 线程去创建纹理。5.5 真机一切正常但模拟器上一片漆黑EGL 配置和 GL 版本不匹配现象同样的代码真机能画出来模拟器上背景色正常但圆形就是不显示也没有任何报错。原因模拟器的 GPU 支持情况差异很大尤其部分模拟器只支持 OpenGL ES 1.0不完整支持 ES 2.0。代码里setEGLContextClientVersion(2)会被忽略或直接创建失败导致着色器无法编译画面停留在清屏色。解决先在模拟器里确认GLES20.glGetString(GLES20.GL_VERSION)是否真的包含 ES 2.0 字符串。如果版本不对可以在模拟器设置里强制开启 GPU 渲染或者干脆用真机调试。这个现象不是代码逻辑本身的问题不用盲目改代码。5.6 画面边缘有锯齿和毛刺抗锯齿的三个落地手段现象圆的边缘有明显的阶梯状锯齿在白底上尤其刺眼。原因三角形光栅化时一个圆被离散成了许多三角形每个三角形边界处的像素自然形成锯齿。听上去是渲染问题但直接改顶点数量并不能根治因为锯齿来自像素采样不是几何精度。解决常见有三个落地手段。第一是给 EGL 配置文件申请多重采样缓冲区这种情况下onDrawFrame里照样画圆最终画面会被硬件平滑。第二是在片段着色器里对边缘像素做 alpha 渐变也就是在圆的边缘计算距离场让硬边变成渐变。第三是在 GLSurfaceView 上层叠加一个抗锯齿的视图层但性能开销最大。对于入门工程首选方案是配置EGLConfigChooser并请求EGL_SAMPLES为 4效果最直接。如果资源工程里已经写了多重采样配置要注意它在部分低端 GPU 上有性能损耗必要时加一个开关方便在性能和画质之间切换。6. 验证与封装把圆形渲染做成可复用工具再用网格法证明它是真圆这一章不讲新概念只讲怎么把前面的代码收拾成能长期使用的工具以及怎么验证自己画出来的东西确实是个圆。很多人写完一个 Demo 就交差了但真正做项目时圆的出现场景远不止一个按钮指示器、头像边框、加载动画、小地图标记点全都需要圆形渲染。与其每次复制粘贴之前的顶点生成函数不如封装成一个独立的方法。先给一个裁剪过的核心封装思路。整套工程里的 CircleRenderer 类会把第 2 章的顶点生成、第 3 章的缓冲初始化、以及第 4 章的矩阵变换合并起来对外只需要暴露setCenter、setRadius、setColor和draw四个方法。顶点只在 setRadius 变化时才重新生成颜色和位置变化都走 uniform 路径。// 对外绘制接口调用方只需要关心圆心、半径和颜色 public void draw(float centerX, float centerY, float radius, float r, float g, float b, float alpha) { // 位置和缩放通过矩阵计算不再重建顶点 Matrix.setIdentityM(mModelMatrix, 0); Matrix.translateM(mModelMatrix, 0, centerX, centerY, 0f); Matrix.scaleM(mModelMatrix, 0, radius, radius, 1f); Matrix.multiplyMM(mMvpMatrix, 0, mProjMatrix, 0, mModelMatrix, 0); GLES20.glUniformMatrix4fv(mvpHandle, 1, false, mMvpMatrix, 0); GLES20.glUniform4f(colorHandle, r, g, b, alpha); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, vertexCount); }封装之后最直观的好处是调用点非常干净。某个按钮、某个道具图标要画一个加载圆环时几行就能搞定不会看到任何顶点数组或 VBO 的细节。接着是验证技巧。很多初学者画完之后只盯着屏幕中心看觉得差不多是圆就收工了。这里我给出一个代价低但有效的验证方法在屏幕上画一个正方形网格参考线正圆的外接正方形边长等于圆的直径圆的边缘在四个方向上恰好相切于网格交点。如果你画的圆和网格之间没有明显偏差圆基本是成立的。验证时可以临时把分段数调小到 16 或 8这时候你就能清楚看到正多边形和网格的差异确认坐标系和投影没有问题后再把分段数恢复到动态计算值。这招对我自己很有用既能直观看到顶点布局也能排查出“圆其实是椭圆但肉眼没看出来”的情况。还有一个非常容易操作的方法把圆的颜色改成半透明在同一个位置上用系统 View 绘制一个标准的圆形对比图。两个圆重叠时如果边缘偏差在 1 像素以内OpenGL 侧的实现就算过关了。我自己的习惯是每次写完一个渲染模块都会做一次这种“几何体验证”而不是画出来好看就直接跳到下一个功能。因为很多图形问题在单一圆上不明显但等做到多圆重叠、边缘对接时再回头修代价就是重新清开所有代码。从那以后每次我调整全局坐标系或重构渲染管线都会强制把“参考网格 半透明对比圆”这套验证流程完整走一遍再用动态参数跑几组真机数据确定圆在不同半径下都维持正确形状才敢放心往下做。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

MATLAB二维Delaunay网格生成:MESH2D轻量可控实战指南

MATLAB二维Delaunay网格生成:MESH2D轻量可控实战指南

简介:MESH2D是一款面向科研人员、工程仿真从业者及高校师生的MATLAB/Octave二维Delaunay网格生成工具,专为解决有限元分析、流体力学模拟、地形建模等场景中复杂几何区域的高质量三角剖分问题而设计。资源包共69个文件,含55个核心MATLAB函数&…

2026/10/15 13:26:57 阅读更多 →
SAP外部作业调度服务实战解析与优化

SAP外部作业调度服务实战解析与优化

1. 项目背景与核心价值在企业级应用环境中,SAP系统的批处理作业(Application Job)管理一直是运维工作的重点和难点。传统的手工调度方式不仅效率低下,还容易因人为失误导致作业执行失败或资源冲突。External Job Scheduling Servi…

2026/10/15 13:26:57 阅读更多 →
VMware桥接模式实现虚拟机远程登录与文件传输

VMware桥接模式实现虚拟机远程登录与文件传输

1. 项目概述:为什么桥接模式是远程协作的“隐形地基”VMware桥接网络模式,远程登录和远程传输文件——这三件事放在一起,不是偶然组合,而是实际工作中最常被卡住的“铁三角”。我带过不少刚接触虚拟化环境的开发者和运维新人&…

2026/10/15 13:26:57 阅读更多 →

最新新闻

AI Agent编排的声明式革命:从Kubernetes思想到YAML任务管理

AI Agent编排的声明式革命:从Kubernetes思想到YAML任务管理

1. 为什么我会盯着 AX 不放:Agent 量的激增带来编排空白先说个前几天的事儿。有位朋友找我帮他们团队看一套多 Agent 系统,代码里密密麻麻全是while True轮询任务状态、手写重试、手写超时控制、用全局字典存每个 Agent 的中间结果。功能是能跑&#xff…

2026/10/15 14:13:17 阅读更多 →
列车运行图自动生成系统:一张图背后的组合优化与落地路径

列车运行图自动生成系统:一张图背后的组合优化与落地路径

简介:面向C开发学习者和铁路信息化相关课程设计者,这份资源实现了一个基于MFC的列车运行图自动生成系统。系统将菜单栏与运行图显示相结合,支持维护列车时刻表、登记晚点信息,并依据时刻表动态绘制固定轴线与红蓝车次斜线&#xf…

2026/10/15 14:13:17 阅读更多 →
RCNN目标检测原理详解:从候选区域到CNN特征提取的完整技术解析

RCNN目标检测原理详解:从候选区域到CNN特征提取的完整技术解析

1. 这篇论文到底解决了什么问题RCNN(Region-based CNN,区域卷积神经网络)是目标检测领域绕不开的一座里程碑。在它出现之前,主流的目标检测方法还停留在手工特征时代,代表方案是DPM(Deformable Part Model&…

2026/10/15 14:13:17 阅读更多 →
.NET应用自适应SQLite 32位/64位/ARM64架构方案

.NET应用自适应SQLite 32位/64位/ARM64架构方案

1. 项目概述:为什么SQLite在.NET里会“挑系统”? “让使用SQLite的.NET应用自适应32位/64位系统”——这句话听起来像一句技术口号,但背后藏着大量.NET开发者踩过的真实坑。我从2012年开始做桌面端数据采集工具,最早用的是.NET F…

2026/10/15 14:13:17 阅读更多 →
贪心算法的数学归纳法证明实测:大模型在区间调度问题最优子结构推导中的表现

贪心算法的数学归纳法证明实测:大模型在区间调度问题最优子结构推导中的表现

很多学弟学妹在刷 LeetCode 时常有一种错觉:贪心算法是所有算法里最简单的,无非就是排个序、写个循环、抓着眼前的最大值或最小值往里塞。但只要参加过大厂后端核心架构岗的终面,或者打过 ICPC 竞赛就知道,面试官如果让你写贪心&a…

2026/10/15 14:13:17 阅读更多 →
MATLAB间断有限元声波模拟:可调试可验证的DG-FEM实现

MATLAB间断有限元声波模拟:可调试可验证的DG-FEM实现

简介:本资源是一套基于Matlab实现二维声波方程数值求解的教学实践代码包,面向计算数学、声学仿真及偏微分方程数值方法方向的高年级本科生与研究生,聚焦间断有限元(DG)方法在波动问题中的应用。资源采用线性基函数构建…

2026/10/15 14:12:15 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →