文章目錄
- 開啟紋理
- 創建紋理
- 綁定紋理
- 生成紋理
- 紋理坐標
- 圖像配置
- 線性插值
- 重復效果
- 限制拉伸
- 完整代碼
在 Android OpenGL ES 中使用紋理(Texture)可以顯著提升圖形渲染的質量和效率。以下是使用紋理的主要好處:
-
增強視覺真實感
紋理可以將復雜的圖像細節(如皮膚、木紋、磚石等)映射到簡單的幾何模型上,避免為每個細節創建復雜的幾何體,從而用較低的計算成本實現高度真實的視覺效果。 -
減少內存占用
通過紋理復用,可以避免為相似的物體重復定義頂點數據。
使用同一張紋理圖渲染多個相同的物體(如樹木、建筑)
利用紋理壓縮技術(如 ETC、ASTC)進一步減少內存占用 -
提高渲染性能
紋理在 GPU 中以高度優化的方式存儲和處理,比動態生成的圖形更高效。例如:
使用預渲染的紋理代替實時計算(如陰影、光照效果)
利用 Mipmapping 技術優化遠處物體的紋理采樣 -
實現復雜特效
紋理不僅可以用于靜態圖像,還能實現各種高級特效:
環境映射(Environment Mapping):模擬反射效果(如鏡面、金屬表面)
法線貼圖(Normal Mapping):通過紋理模擬表面細節,增加立體感
程序紋理(Procedural Textures):動態生成紋理(如火焰、煙霧)
多重紋理(Multi-texturing):疊加多個紋理創建復合效果(如地形 + 植被) -
簡化模型復雜度
使用紋理可以將細節從幾何模型轉移到紋理圖像中,從而:
減少頂點數量,降低 GPU 處理負擔
簡化模型設計流程,提高開發效率
支持更復雜的視覺效果,而不增加渲染負擔 -
跨平臺兼容性
OpenGL ES 紋理機制在不同設備上具有良好的一致性,使得:
紋理資源可以在不同 Android 設備間復用
便于將應用移植到其他支持 OpenGL ES 的平臺 -
靈活的紋理控制
通過調整紋理參數,可以實現豐富的視覺變化:
過濾模式(Filtering):控制紋理縮放時的質量(如線性插值、最近鄰采樣)
環繞模式(Wrapping):定義紋理坐標超出 [0,1] 范圍時的行為(重復、鏡像等)
紋理混合(Blending):將紋理與顏色或其他紋理組合
開啟紋理
為了使用紋理,需要開啟一些開關啟動一些功能。
- 打開2D貼圖
激活 OpenGL ES 上下文的 2D 紋理功能
允許后續的紋理操作(如綁定紋理、設置紋理參數)生效
確保在繪制時使用紋理坐標對紋理進行采樣gl.glEnable(GL10.GL_TEXTURE_2D)
- 打開混色
在 Android OpenGL ES 中,gl.glEnable(GL10.GL_BLEND)
用于啟用顏色混合功能,這是實現半透明效果、粒子系統、紋理疊加等視覺效果的關鍵技術。gl.glEnable(GL10.GL_BLEND)
- 設置混色
下面方法是 Android OpenGL ES 中用于設置標準透明度混合(Alpha Blending)的核心方法。
這個設置允許你實現半透明效果,讓新繪制的物體能夠以透明方式與背景融合。gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA)
- OpenGL 支持的混色方案如下
因子 | 描述 |
---|---|
GL_ZERO | 因子 = (0, 0, 0, 0) |
GL_ONE | 因子 = (1, 1, 1, 1) |
GL_SRC_COLOR | 因子 = (Rs, Gs, Bs, As) (源顏色) |
GL_ONE_MINUS_SRC_COLOR | 因子 = (1-Rs, 1-Gs, 1-Bs, 1-As) (1 - 源顏色) |
GL_DST_COLOR | 因子 = (Rd, Gd, Bd, Ad) (目標顏色) |
GL_ONE_MINUS_DST_COLOR | 因子 = (1-Rd, 1-Gd, 1-Bd, 1-Ad) (1 - 目標顏色) |
GL_SRC_ALPHA | 因子 = (As, As, As, As) (源 Alpha 值) |
GL_ONE_MINUS_SRC_ALPHA | 因子 = (1-As, 1-As, 1-As, 1-As) (1 - 源 Alpha 值) |
GL_DST_ALPHA | 因子 = (Ad, Ad, Ad, Ad) (目標 Alpha 值) |
GL_ONE_MINUS_DST_ALPHA | 因子 = (1-Ad, 1-Ad, 1-Ad, 1-Ad) (1 - 目標 Alpha 值) |
通過合理選擇混色方案,你可以在 Android OpenGL ES 應用中實現從簡單半透明到復雜特效的各種視覺效果。
創建紋理
// 存儲紋理IDprivate val texture = IntArray(1)// 用于生成紋理ID是臨時存儲val textureArray = IntArray(1)// 生成紋理ID,存儲到textureArray 中。gl.glGenTextures(1, textureArray, 0)// 保存ID到成員變量,后續使用texture[0] = textureArray[0]
生成紋理:glGenTextures 是 OpenGL 的核心函數,用于創建紋理對象
第一個參數 1:表示生成 1 個紋理
第二個參數 textureArray:存儲生成的紋理 ID
第三個參數 0:表示從數組的第 0 個位置開始存儲
綁定紋理
-
紋理綁定的基本概念
紋理綁定是指將一個紋理對象(通過 glGenTextures 生成)關聯到當前 OpenGL 上下文的過程。綁定后,所有對紋理的操作(如設置參數、上傳數據)都將作用于這個紋理對象。OpenGL ES 使用紋理單元(Texture Unit)機制來支持同時使用多個紋理。每個紋理單元可以綁定一個紋理對象,通過 glActiveTexture 切換當前使用的紋理單元。
紋理綁定本質
- 將一個紋理對象(由 textureId 標識)關聯到當前的紋理目標(如 GL_TEXTURE_2D)
- 狀態覆蓋:每次調用 glBindTexture 都會覆蓋之前的綁定狀態
- 最終生效:在繪制時,OpenGL 使用的是最后一次綁定的紋理
- 綁定紋理函數
gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])
第一個參數:紋理目標:指定紋理的類型,常見的有:
GL_TEXTURE_2D:二維紋理(最常用)
GL_TEXTURE_CUBE_MAP:立方體貼圖(用于天空盒、反射等)
GL_TEXTURE_3D:三維紋理(OpenGL ES 3.0+ 支持)
第二個參數:紋理 ID:通過 glGenTextures 生成的唯一標識符,代表一個紋理對象
- 為什么需要綁定紋理?
OpenGL 使用狀態機模型,所有操作都作用于當前綁定的對象。
綁定紋理的主要目的是:
- 設置紋理參數:在綁定時設置過濾模式、環繞模式等
- 上傳紋理數據:將圖像數據(如 Bitmap)加載到紋理中
- 在繪制時使用紋理:確保正確的紋理被應用到幾何體上
生成紋理
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmapTexture, 0)
該方法參數如下:
public static void texImage2D(int target, // 紋理目標類型int level, // Mipmap 級別Bitmap bitmap, // 源 Bitmap 對象int border // 邊框寬度(必須為 0)
)
- 對bitmap的尺寸要求
2 的冪尺寸:傳統 OpenGL ES 1.x 和部分設備要求紋理尺寸必須是 2 的冪(如 64×64、256×512)。若使用非 2 的冪(NPOT)紋理,可能需要額外設置紋理參數.
OpenGL ES 2.0+ 支持:現代設備通常支持任意尺寸(需檢查 GL_MAX_TEXTURE_SIZE),但為兼容性建議使用 2 的冪尺寸。
紋理坐標
在 OpenGL ES 中,紋理坐標(Texture Coordinates)用于將 2D 圖像(紋理)映射到 3D 模型的表面。理解紋理坐標的工作原理對實現正確的紋理映射至關重要。
-
坐標系范圍:紋理坐標使用標準化的 (s, t) 坐標系統,范圍從 (0, 0) 到 (1, 1)。
- s:水平方向(類似屏幕的 X 軸)。
- t:垂直方向(類似屏幕的 Y 軸)。
-
原點位置:(0, 0) 位于紋理的左下角,(1, 1) 位于右上角。
-
與屏幕坐標的差異:屏幕坐標通常以左上角為原點,而 OpenGL 紋理坐標以左下角為原點,這可能導致紋理顯示時上下顛倒(需特別處理)。
-
紋理坐標與頂點坐標的映射
- 紋理坐標需要與頂點坐標一一對應,以確定紋理如何被拉伸或扭曲到模型表面。以下是常見形狀的映射示例:
圖中是一個正方形,對應原點為中心點,長度為1,對應的紋理坐標,可見左下角頂點坐標(-1,-1,0) 對應的紋理坐標的(0,0)
- 紋理坐標需要與頂點坐標一一對應,以確定紋理如何被拉伸或扭曲到模型表面。以下是常見形狀的映射示例:
頂點坐標 紋理坐標
(-1, 1, 0) ------------ (0, 1)| || || |
(-1,-1, 0) ------------ (0, 0)------------(1,-1, 0) (1, 0)|||------------(1, 1, 0) (1, 1)
對應代碼如下:
// 頂點坐標(x, y, z)
float[] vertices = {-1f, 1f, 0f, // 左上-1f, -1f, 0f, // 左下1f, -1f, 0f, // 右下1f, 1f, 0f // 右上
};// 紋理坐標(s, t)
float[] texCoords = {0f, 1f, // 左上0f, 0f, // 左下1f, 0f, // 右下1f, 1f // 右上
};
圖像配置
線性插值
在 OpenGL ES 中,紋理的線性插值(Linear Interpolation) 是一種紋理過濾技術,用于在紋理被縮放時生成平滑的視覺效果。與最近鄰采樣(GL_NEAREST)相比,線性插值通過計算相鄰紋理像素(紋素)的加權平均值來減少鋸齒和馬賽克現象,尤其適用于紋理被縮小或旋轉的場景。
-
最近鄰采樣(Nearest Neighbor):
- 直接選擇離采樣點最近的單個紋素值。
- 優點:性能高,適合像素藝術風格。
- 缺點:紋理縮放時會產生明顯鋸齒或馬賽克。
-
線性插值(Bilinear Filtering):
- 在水平和垂直方向各取 2×2 個紋素,計算采樣點周圍紋素的加權平均值。
- 優點:平滑過渡,減少鋸齒。
- 缺點:模糊化(尤其在大幅縮小時),性能略低于最近鄰。
-
啟用線性插值
- 通過 glTexParameteri() 設置紋理過濾模式:
// 綁定紋理后設置過濾模式
gl.glBindTexture(GL10.GL_TEXTURE_2D, textureId);// 設置縮小過濾為線性插值(紋理被縮小時)
gl.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);// 設置放大過濾為線性插值(紋理被放大時)
gl.glTexParameteri(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
**GL_LINEAR 代表OpenGL采用簡單的線性插值方式調整圖像。**
為了將圖標貼到正方體的一個面,繪制時根據面來設置紋理,最后會貼出全部代碼。增加紋理部分如下:
private fun setTexture(gl: GL10) {gl.glEnable(GL10.GL_TEXTURE_2D)gl.glEnable(GL10.GL_BLEND)gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA)val textureArray = IntArray(1)gl.glGenTextures(1, textureArray, 0)texture[0] = textureArray[0]// 綁定紋理gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])// 當需要縮小時采用線性插值方式gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR)// 當需要放大時采用線性插值的方式gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR)GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmapTexture, 0)}
繪制時采用紋理的代碼如下(只針對其中一個面):
// 目標面啟用紋理gl.glEnable(GL10.GL_TEXTURE_2D)gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY)gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textCoordsSquare)
public void glTexCoordPointer(int size, // 每個紋理坐標的分量數int type, // 數據類型int stride, // 相鄰兩個紋理坐標之間的字節偏移量java.nio.Buffer pointer // 數據緩沖區
)
-
size(每個紋理坐標的分量數)取值:必須為 2、3 或 4。
- 2表示 (s, t) 坐標(最常用)。
- 3表示 (s, t, r) 坐標(用于 3D 紋理,OpenGL ES 中較少使用)。
- 4表示 (s, t, r, q) 坐標(用于齊次坐標,OpenGL ES 中極少使用)。
-
pointer(數據緩沖區)
- 類型:java.nio.Buffer(通常為 FloatBuffer)。
- 含義:存儲紋理坐標數據的緩沖區,必須為直接緩沖區(Direct Buffer)。
繪制效果如下:Android的Logo。
備注:如果一個四邊形的圖片通過紋理的方式貼到三角形上,則選取正方形的三個角的紋理點,對應三角形坐標點。選取的三角形部分會繪制到對應三角形上。
重復效果
先將紋理坐標超過 1.0
// 紋理坐標(左下角為(0,0),右上角為(1,1))private val textureCoords = floatArrayOf(0.0f, 2f, // 左下 V02f, 2f, // 右下 V12f, 0.0f, // 右上 V20.0f, 0.0f // 左上 V3)
然后增加設置重復設置
// 設置紋理環繞模式(重復)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT)
效果如下:
GL_REPEAT:紋理會在超出部分重復平鋪。例如,紋理坐標為 1.5 時,會使用 0.5 處的紋理數據,從而實現紋理在整個表面上重復鋪設的效果,常用于制作無縫紋理背景。
限制拉伸
OpenGLES可以設置簡單地將紋理坐標超過1.0的值限制為1.0,任何低于0.0的值限制為0.0。這實際會引起邊沿像素重復。
將重復設置修改為限制拉伸設置
gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE)
效果如下:只顯示了左上角一個圖標。
完整代碼
class SquareWithTextureRenderer(private val context: Context) : GLSurfaceView.Renderer {private val vertexBuffer: FloatBufferprivate val indexBuffer: ShortBufferprivate val textureCoordsBuffer: FloatBufferprivate val normalsBuffer: FloatBufferprivate val texture: IntArrayprivate val mBitmapTexture: Bitmapprivate var angle = 0f// 正方形的4個頂點坐標(中心在原點,邊長為1)private val vertices = floatArrayOf(-0.5f, -0.5f, 0.0f, // 左下 V00.5f, -0.5f, 0.0f, // 右下 V10.5f, 0.5f, 0.0f, // 右上 V2-0.5f, 0.5f, 0.0f // 左上 V3)// 兩個三角形組成正方形的頂點索引private val indices = shortArrayOf(0, 1, 2, // 第一個三角形2, 3, 0 // 第二個三角形)// 正方形頂點的法線(全部朝向屏幕外,即Z軸正方向)private val normals = floatArrayOf(0.0f, 0.0f, 1.0f, // 所有頂點法線相同0.0f, 0.0f, 1.0f,0.0f, 0.0f, 1.0f,0.0f, 0.0f, 1.0f)// 紋理坐標(左下角為(0,0),右上角為(1,1))private val textureCoords = floatArrayOf(0.0f, 2f, // 左下 V02f, 2f, // 右下 V12f, 0.0f, // 右上 V20.0f, 0.0f // 左上 V3)init {// 初始化頂點緩沖區val vbb = ByteBuffer.allocateDirect(vertices.size * 4)vbb.order(ByteOrder.nativeOrder())vertexBuffer = vbb.asFloatBuffer()vertexBuffer.put(vertices)vertexBuffer.position(0)// 初始化索引緩沖區val ibb = ByteBuffer.allocateDirect(indices.size * 2)ibb.order(ByteOrder.nativeOrder())indexBuffer = ibb.asShortBuffer()indexBuffer.put(indices)indexBuffer.position(0)// 初始化紋理坐標緩沖區val tcb = ByteBuffer.allocateDirect(textureCoords.size * 4)tcb.order(ByteOrder.nativeOrder())textureCoordsBuffer = tcb.asFloatBuffer()textureCoordsBuffer.put(textureCoords)textureCoordsBuffer.position(0)// 初始化法線緩沖區val nb = ByteBuffer.allocateDirect(normals.size * 4)nb.order(ByteOrder.nativeOrder())normalsBuffer = nb.asFloatBuffer()normalsBuffer.put(normals)normalsBuffer.position(0)// 加載紋理圖片texture = IntArray(1)mBitmapTexture = BitmapFactory.decodeResource(context.resources, R.drawable.ic_l)}override fun onSurfaceCreated(gl: GL10, config: EGLConfig) {gl.glClearColor(0.5f, 0.5f, 0.5f, 1.0f)gl.glEnable(GL10.GL_DEPTH_TEST)gl.glEnableClientState(GL10.GL_VERTEX_ARRAY)gl.glEnableClientState(GL10.GL_NORMAL_ARRAY)setupLight(gl)setMaterial(gl)setTexture(gl)}private fun setTexture(gl: GL10) {gl.glEnable(GL10.GL_TEXTURE_2D)gl.glEnable(GL10.GL_BLEND)gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA)val textureArray = IntArray(1)gl.glGenTextures(1, textureArray, 0)texture[0] = textureArray[0]// 綁定紋理gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])// 設置紋理過濾gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR)// 設置紋理環繞模式
// gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT)
// gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE)gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE)// 加載紋理GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmapTexture, 0)mBitmapTexture.recycle() // 紋理加載后回收Bitmap}override fun onSurfaceChanged(gl: GL10, width: Int, height: Int) {gl.glViewport(0, 0, width, height)gl.glMatrixMode(GL10.GL_PROJECTION)gl.glLoadIdentity()val aspectRatio = width.toFloat() / heightGLU.gluPerspective(gl, 45.0f, aspectRatio, 0.1f, 1000.0f)gl.glMatrixMode(GL10.GL_MODELVIEW)gl.glLoadIdentity()}override fun onDrawFrame(gl: GL10) {gl.glClear(GL10.GL_COLOR_BUFFER_BIT or GL10.GL_DEPTH_BUFFER_BIT)gl.glLoadIdentity()gl.glShadeModel(GL10.GL_SMOOTH)gl.glTranslatef(0.0f, 0f, -3.0f) // 拉近正方形,使其更清晰
// gl.glRotatef(angle, 0.0f, 0.0f, 1.0f) // 繞Z軸旋轉// angle += 1.0f // 旋轉角度遞增// 設置頂點、法線和紋理坐標gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer)gl.glNormalPointer(GL10.GL_FLOAT, 0, normalsBuffer)gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY)gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureCoordsBuffer)// 啟用紋理gl.glEnable(GL10.GL_TEXTURE_2D)gl.glBindTexture(GL10.GL_TEXTURE_2D, texture[0])// 繪制正方形(兩個三角形)gl.glDrawElements(GL10.GL_TRIANGLES, indices.size,GL10.GL_UNSIGNED_SHORT, indexBuffer)// 禁用紋理和緩沖區gl.glDisable(GL10.GL_TEXTURE_2D)gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY)}/*** 設置光效*/private fun setupLight(gl: GL10) {// 啟用光照gl.glEnable(GL10.GL_LIGHTING)gl.glEnable(GL10.GL_LIGHT0)gl.glEnable(GL10.GL_SPECULAR)// 設置環境光val ambientLight = floatArrayOf(0.2f, 0.2f, 0.2f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLight, 0)// 設置漫反射光val diffuseLight = floatArrayOf(0.8f, 0.8f, 0.8f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseLight, 0)// 設置高光val specularLight = floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularLight, 0)// 設置光源位置(位于正方形前方)val lightPosition = floatArrayOf(0.0f, 0.0f, 1.0f, 0.0f)gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightPosition, 0)}private fun setMaterial(gl: GL10) {// 環境元素val ambientLight = floatArrayOf(0f, 0.1f, 0.9f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, ambientLight, 0)// 散射元素val diffuseLight = floatArrayOf(0.9f, 0f, 0.1f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, diffuseLight, 0)// 高光元素val specularLight = floatArrayOf(0.9f, 0.9f, 0f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, specularLight, 0)// 反光度gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS, 25F)// 自發光val emissionLight = floatArrayOf(0.0f, 0.4f, 0f, 1.0f)gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_EMISSION, emissionLight, 0)}
}