Open GL ES->以指定点为中心缩放图片纹理的完整图解

以指定点为中心缩放的完整图解

场景设定

目标:用户点击屏幕右侧某点,放大 2 倍,该点保持不动。

kotlin 复制代码
/**
* 以指定点为中心进行缩放动画
* @param targetScale 目标缩放值(绝对值)
* @param centerX 缩放中心X(屏幕坐标)
* @param centerY 缩放中心Y(屏幕坐标)
* @param duration 动画时长
*/
fun animateScaleWithCenter(
   targetScale: Float,
   centerX: Float,
   centerY: Float,
   duration: Long = ANIMATION_DURATION,
   requestCallback: () -> Unit
) {

   animator?.cancel()
   animator = null

   val startScaleX = scaleX
   val startScaleY = scaleY
   val startTranslateX = translateX
   val startTranslateY = translateY

   // 屏幕坐标 → OpenGL 归一化坐标
   val normalizedX = (centerX / mWidth) * 2f - 1f
   val normalizedY = 1f - (centerY / mHeight) * 2f

   // 计算缩放前该点在模型空间的位置
   val pointBeforeX = (normalizedX - startTranslateX) / startScaleX
   val pointBeforeY = (normalizedY - startTranslateY) / startScaleY

   // 限制目标缩放范围
   val clampedTargetScale = targetScale.coerceIn(MIN_SCALE, MAX_SCALE)

   // 计算目标平移量
   val pointAfterX = pointBeforeX * clampedTargetScale
   val pointAfterY = pointBeforeY * clampedTargetScale
   val targetTranslateX = normalizedX - pointAfterX
   val targetTranslateY = normalizedY - pointAfterY

   animator = ValueAnimator.ofFloat(0f, 1f).apply {
       this.duration = duration
       interpolator = DecelerateInterpolator()

       addUpdateListener { animator ->
           val fraction = animator.animatedValue as Float

           scaleX = startScaleX + (clampedTargetScale - startScaleX) * fraction
           scaleY = startScaleY + (clampedTargetScale - startScaleY) * fraction
           translateX = startTranslateX + (targetTranslateX - startTranslateX) * fraction
           translateY = startTranslateY + (targetTranslateY - startTranslateY) * fraction
           computeMVPMatrix()
           requestCallback()
       }
           start()
    }
  • 屏幕尺寸:1080 x 1920
  • 点击位置:(810, 960) ← 屏幕右侧中间
  • 当前状态:scale = 1.0, translate = 0.0
  • 目标状态:scale = 2.0, translate = ?

第一步:屏幕坐标 → 归一化坐标

坐标转换

kotlin 复制代码
// 用户点击的屏幕坐标
centerX = 810f
centerY = 960f

// 转换为 OpenGL 归一化坐标
normalizedX = (810 / 1080) * 2 - 1 = 0.5
normalizedY = 1 - (960 / 1920) * 2 = 0.0

图示

结果:触摸点在归一化空间的坐标是 (0.5, 0.0)

kotlin 复制代码
屏幕坐标系                          OpenGL 归一化坐标系
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

(0, 0) ──────────► (1080, 0)            Y ▲
  │                                       │
  │                                       │
  │                              (-1, 1) ┌┼┐ (1, 1)
  │           ● (810, 960)               │●│ (0.5, 0)
  │                              ────────┼─┼────────► X
  │                             (-1,-1) └┼┘ (1, -1)
  ▼                                       │
(0, 1920) ───────► (1080, 1920)

第二步:理解当前的变换状态

当前变换

kotlin 复制代码
scaleX = 1.0      // 没有缩放
translateX = 0.0  // 没有平移
变换公式
归一化坐标 = 模型坐标 × scale + translate

图示:当前状态

kotlin 复制代码
模型空间                变换                归一化空间
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

        Y ▲                              Y ▲
          │                                │
          │                                │
  (-1, 1) ┌┼┐ (1, 1)              (-1, 1) ┌┼┐ (1, 1)
          │●│ (0.5, 0)   ×1.0 +0.0        │●│ (0.5, 0)
  ────────┼─┼────────► X  ═════►   ────── ┼─┼────────► X
  (-1,-1) └┼┘ (1, -1)              (-1,-1)└┼┘ (1, -1)
          │                                │
模型点 (0.5, 0) 归一化点 (0.5, 0)
当前状态 模型空间和归一化空间完全一致(因为 scale=1, translate=0

第三步:计算模型空间坐标(关键!)

为什么要计算?

问题:用户点击的归一化坐标 (0.5, 0) 对应的是模型空间的哪个点?

答案:需要逆向变换!

kotlin 复制代码
逆向变换公式
// 正向:模型 → 归一化
normalized = model * scale + translate

// 逆向:归一化 → 模型
model = (normalized - translate) / scale
计算
pointBeforeX = (normalizedX - startTranslateX) / startScaleX
             = (0.5 - 0.0) / 1.0
             = 0.5

pointBeforeY = (normalizedY - startTranslateY) / startScaleY
             = (0.0 - 0.0) / 1.0
             = 0.0

图示:逆向变换

kotlin 复制代码
归一化空间                逆向变换              模型空间
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

        Y ▲                              Y ▲
          │                                │
          │                                │
  (-1, 1) ┌┼┐ (1, 1)              (-1, 1) ┌┼┐ (1, 1)
          │●│ (0.5, 0)   (x-0)/1.0        │●│ (0.5, 0)
  ────────┼─┼────────► X  ◄═════   ───────┼─┼────────► X
  (-1,-1) └┼┘ (1, -1)              (-1,-1)└┼┘ (1, -1)
          │                                │
归一化点 (0.5, 0) 模型点 (0.5, 0)
结果 触摸点对应的模型空间坐标是 (0.5, 0.0)
重要 这个 (0.5, 0.0) 是模型上的固定点,不会因为缩放而改变!

第四步:缩放后模型点的新位置

缩放操作

kotlin 复制代码
targetScale = 2.0

// 缩放后,模型点在归一化空间的新位置
pointAfterX = pointBeforeX * clampedTargetScale
            = 0.5 * 2.0
            = 1.0

pointAfterY = pointBeforeY * clampedTargetScale
            = 0.0 * 2.0
            = 0.0

图示:缩放模型点

kotlin 复制代码
模型空间                    缩放 ×2.0             归一化空间(无平移)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

        Y ▲                                  Y ▲
          │                                    │
          │                                    │
  (-1, 1) ┌┼┐ (1, 1)                  (-1, 1) ┌┼┐ (1, 1) ●
          │●│ (0.5, 0)      ×2.0              │ │         (1.0, 0)
  ────────┼─┼────────► X    ═════►    ────────┼─┼────────────────► X
  (-1,-1) └┼┘ (1, -1)                  (-1,-1)└┼┘ (1, -1)
           │                                   │
模型点 (0.5, 0) 缩放后 (1.0, 0) ← 超出屏幕了!
问题 缩放后,模型点 (0.5, 0) 变成了归一化坐标 (1.0, 0),跑到屏幕右边缘了!
期望 我们希望它保持在原来的位置 (0.5, 0)

第五步:计算目标平移量

计算平移

kotlin 复制代码
// 目标:让模型点保持在原来的归一化位置
targetTranslateX = normalizedX - pointAfterX
                 = 0.5 - 1.0
                 = -0.5

targetTranslateY = normalizedY - pointAfterY
                 = 0.0 - 0.0
                 = 0.0

图示:应用平移

kotlin 复制代码
缩放后(无平移)                      应用平移 -0.5              最终结果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

        Y ▲                              Y ▲                    Y ▲
          │                                │                      │
          │                                │                      │
  (-1, 1) ┌┼┐ (1, 1) ●            (-1, 1) ┌┼┐ (1, 1)     (-1, 1) ┌┼┐ (1, 1)
          │ │         (1.0, 0)            │ │●                   │●│ (0.5, 0)
  ────────┼─┼────────────► X   + (-0.5)   │ │(0.5, 0)    ────────┼─┼────────► X
  (-1,-1) └┼┘ (1, -1)        ═════►       │ │            (-1,-1) └┼┘ (1, -1)
          │                                │                      │
点在右边缘 向左平移 0.5 点,回到原位置!✅
结果 通过平移 -0.5,模型点回到了原来的位置 (0.5, 0)

第六步:验证最终变换

最终状态

kotlin 复制代码
scaleX = 2.0
translateX = -0.5
验证公式
// 模型点 (0.5, 0) 经过变换后的归一化坐标
normalizedX = pointBeforeX * scaleX + translateX
            = 0.5 * 2.0 + (-0.5)
            = 1.0 - 0.5
            = 0.5  ✅

// 确实保持在原位置!

完整变换链图示

kotlin 复制代码
  模型空间             缩放 ×2.0         平移 -0.5          归一化空间
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

    Y ▲                Y ▲                Y ▲                Y ▲
      │                  │                  │                  │
      │                  │                  │                  │
(-1,1)┌┼┐(1,1)   (-1,1) ┌┼┐ (1,1) ●  (-1,1)┌┼┐(1,1)   (-1,1) ┌┼┐ (1,1)
      │●│(0.5,0)        │ │        (1,0)   │●│(0.5,0)        │●│(0.5,0)
──────┼─┼──────► X ─────┼─┼────────────► X ┼─┼──────► X ─────┼─┼──────► X
(-1,-1)└┼┘(1,-1) (-1,-1)└┼┘ (1,-1)  (-1,-1)└┼┘(1,-1)  (-1,-1)└┼┘(1,-1)
      │                  │                  │                  │
模型点 缩放后跑偏 平移回来 最终位置
(0.5, 0) (1.0, 0) (0.5, 0) (0.5, 0) ✅

完整流程总结

kotlin 复制代码
数据流
步骤1:屏幕坐标 (810, 960)
   ↓ 转换
步骤2:归一化坐标 (0.5, 0.0)
   ↓ 逆向变换
步骤3:模型坐标 (0.5, 0.0)  ← pointBefore
   ↓ 缩放 ×2.0
步骤4:缩放后位置 (1.0, 0.0)  ← pointAfter
   ↓ 计算平移
步骤5:目标平移 -0.5  ← targetTranslate
   ↓ 应用变换

核心要点

kotlin 复制代码
pointBefore 的作用
pointBefore = 触摸点在模型空间的坐标
            = 模型上的固定点
            = 缩放的"锚点"

为什么需要它?

模型空间的坐标是固定的,模型顶点不会因为缩放而改变,只有变换矩阵在改变,需要知道触摸点对应的模型点

通过逆向变换计算

kotlin 复制代码
pointBefore = (normalized - translate) / scale

缩放后调整平移,保持模型点位置不变

缩放会改变模型点的屏幕位置

通过平移把它拉回原位置

kotlin 复制代码
translate = normalized - (pointBefore * targetScale)

数学本质

kotlin 复制代码
目标:normalized = pointBefore * targetScale + targetTranslate
求解:targetTranslate = normalized - pointBefore * targetScale
相关推荐
feifeigo1231 小时前
MATLAB实现两组点云ICP配准
开发语言·算法·matlab
fengfuyao9851 小时前
粒子群算法(PSO)求解标准VRP问题的MATLAB实现
开发语言·算法·matlab
编程修仙1 小时前
第十一篇 Spring事务
xml·java·数据库·spring
7哥♡ۣۖᝰꫛꫀꪝۣℋ1 小时前
Spring Boot ⽇志
java·spring boot·后端
清晓粼溪1 小时前
Mybatis02:核心功能
java·mybatis
weisonx1 小时前
为什么要多写文章博客
java·c++
zhangphil1 小时前
Kotlin协程cancel取消正在运行的并行Job
kotlin
介一安全1 小时前
【Frida Android】实战篇11:企业常用加密场景 Hook(1)
android·网络安全·逆向·安全性测试·frida
峥嵘life1 小时前
Android EDLA 认证测试内容详解
android