前言
最近我们实现的几篇文章主要聚焦在图片分片处理和局部处理,以及3D运动,主要涉及的是贴图技巧和3D立体顶点绘制,其实这个围绕的是一个主线,给3D物体贴图。Canvas 作为2D坐标系绘制工具来绘制3D,其实有大量的工作要做的,常见的3D图形工具open gl es显然好用的多,但是Canvas 也有他自身的好处就是学习门槛低,另外也可以很快速转化为View布局,因此有他本身存在的价值。
看本篇之前也可以看看其他文章,当然不看也不影响看本篇。
本篇的主题是绘制凸镜效果,本篇会用到一个难度较高的Canvas api来绘制。
至于凹镜,没找到合适的方法,包括找了chat-gpt协助,它给的代码比想象的差的多。所以暂时不要慌,人类目前还是领先的。对于写代码这块儿,chat-gpt不仅能处理好上下文问题,还能连续追问,但是给的答案你会发现,越追问结果的自信度越低,因为代码难度也是提升的,这很符合人类思考逻辑,简单的事不需要记忆就能给出,困难的事还得复盘推理,不过话说回来,他的自信度降低的结果依然很具有参考和学习价值,至少可以做些启发性思考,然后去拓展。
当然,本篇的凸镜效果也和chatgpt关系不大,所以aigc和人工智能的路还很长。
凸镜预览
凸镜效果图2
原图1
凸镜效果图2
原图2:
目前效果比较好的是尺寸正方形的图片,比如上面的图基本都是接近720P的正方形,第一张原图稍微差点,为什么这么说呢?
在测试过程中发现,正方形的效果在使用Shader+裁剪之后,锯齿就不明显了,但是非正方形图片就会有裁剪不到的地方,不过也可以优化算法,在正方形区域拉伸,其他区域不动。
原理
学过英文的都知道,英语中对事物的区分有时候是特别细致入微的,比如中文的网可以代表多种意思,但是在英语中有net、network、web等,这也是英文作为交流工具的的优势之一,不过也会产生一些近义词,会变成学习者的负担,但比起同字不同意的语言,显然要简单的多。而我们今天也是使用网格工具。说到网格,大家肯定知道Grid,但很少人知道mesh,可能做过后端部署的人知道mesh,其实这两者也是有差异的,Grid属于固定不可调整的网格,而mesh属于可以拉伸的和调整的网格,所以我们用到的工具事drawBitmapMesh。
drawBitmapMesh 怎么用,能做什么
我们知道贝塞尔曲线,其通过控制点,要求两点之间的连线同时到达终点,最终可以绘制出更多矢量物体,计算机中大量的字体是通过贝塞尔控制点来实现。而Mesh也是通过网格控制点(注意不是像素点)拉伸图形,使得图形变形,和贝塞尔的区别是,Mesh是网格点,仅能作用于图片,且无法矢量化。
drawBitmapMesh 主要用在图像特殊效果绘制,比如波浪、窗帘、甚至有人用作翻书特效,甚至似乎也可以用来作物理仿真,不过涉及算法太复杂,当然是比不上UE或者Unity这些3D软件。
凸镜实现
凸镜实现其实很简单,只需要将图片两侧的x控制点往图片中线位置拉伸,但是拉伸的幅度随着纵向靠近中心点反而减小,当然Y轴也可以,不过这样人像会显得很扁平。在人类视觉中,图片纵向拉伸的可接受度是高于横向拉伸的,这也是为什么你看不惯胖子但喜欢大长腿的原因,所以均衡饮食和有效锻炼还是很重要的。
我们这次其实也是用了和老祖宗类似的方法,就是平分直径,每纵向走一步,那么拉伸的可分配范围是用勾股定理就可算出来
horizontal = sqrt(R * R - (R - offset) * (R-offset)) * 2;
R- 大圆半径 sqrt - 平方根函数
为什么✖️2,主要还是你要理解,平分的范围是不是圆的,下图橘色线条是平分的线段,肯定需要x2
接下来进行平分橘色线条长度
(x* (horizontal / WIDTH))
方法总结
网格Y轴保持不变,x轴拉伸 尽可能选正方形图片 x轴网格平分长度缩小范围长度计算公式 sqrt(R * R - (R - offset) * (R-offset)) * 2 x轴 平分公式
坑点
平分完horizontal你会看到一半的图片,原因是你从x=0的位置偏移的,导致图片整体向左移动了,所以你需要将图片,这一步也是至关重要的一步。
scss
float fx = (float) ((bitmapWidth - horizontal)/2 + (x* (horizontal / WIDTH))); // 偏移
具体实现
初始化
java
//横向、纵向划分格数:50*50
private static final int WIDTH = 50;
private static final int HEIGHT = 50;
//顶点数:81*81
private final int COUNT = (WIDTH + 1) * (HEIGHT + 1);
//顶点坐标数组
private final float[] orig = new float[COUNT * 2];
//转换后顶点坐标数组
private final float[] verts = new float[COUNT * 2];
绘制
在这一步我们使用了Shader,主要原因是拉伸图片也会产生轻微的锯齿,为了避免锯齿,需要对圆做下裁剪,减除锯齿区域
绘制到Bitmap上
注意,自己创建的Canvas 默认是软件绘制,因此一定要小心不当的操作,比如saveLayer导致画不出来,setLayerType对其无作用。
java
mShaderBitmap.eraseColor(Color.TRANSPARENT);
float radius = Math.min(mBitmap.getWidth(), mBitmap.getHeight()) / 2f;
int bitmapWidth = mBitmap.getWidth();
int bitmapHeight = mBitmap.getHeight();
int index = 0;
for (int y = 0; y <= HEIGHT; y++) {
float fy = y * (1f * bitmapHeight / HEIGHT);
float step = Math.abs(radius - fy);
double horizontal = Math.sqrt(radius * radius - step * step) * 2; //直径
for (int x = 0; x <= WIDTH; x++) {
float fx = (float) ((bitmapWidth - horizontal)/2 + (x* (horizontal / WIDTH))); // 偏移
verts[index * 2 + 0] = fx;
verts[index * 2 + 1] = fy;
index++;
}
}
bitmapCanvas.drawBitmapMesh(mBitmap,WIDTH,HEIGHT,verts,0,null,0,mCommonPaint);
二次编辑并绘制到Canvas上
上面讲过会有锯齿,因此这里剪掉锯齿,裁掉1-2个像素一般足以,除非你的图片不是正方形。
scss
int save = canvas.save();
//下面注释掉的代码会有轻微锯齿
// matrix.setTranslate((getWidth() - mShaderBitmap.getWidth())/2f,(getHeight() - mShaderBitmap.getHeight())/2f);
// canvas.drawBitmap(mShaderBitmap,matrix,mCommonPaint);
//将图片平移到中心位置
matrix.setTranslate((getWidth() - mShaderBitmap.getWidth())/2f,(getHeight() - mShaderBitmap.getHeight())/2f);
BitmapShader bitmapShader = new BitmapShader(mShaderBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//移动Shader,具体用法参考放文章开头说的大镜效果
bitmapShader.setLocalMatrix(matrix);
mCommonPaint.setShader(bitmapShader);
canvas.drawCircle(getWidth()/2f,getHeight()/2f,radius-2,mCommonPaint);
mCommonPaint.setShader(null);
canvas.restoreToCount(save);
另外我们也要注意这里对Shader进行了平移
java
//将图片平移到中心位置
matrix.setTranslate((getWidth() - mShaderBitmap.getWidth())/2f,(getHeight() - mShaderBitmap.getHeight())/2f);
//将图片平移到中心位置
matrix.setTranslate((getWidth() - mShaderBitmap.getWidth())/2f,(getHeight() - mShaderBitmap.getHeight())/2f);
BitmapShader bitmapShader = new BitmapShader(mShaderBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//移动Shader,具体用法参考放文章开头说的大镜效果
bitmapShader.setLocalMatrix(matrix);
总结
本篇到这里基本就结束了,drawBitmapMesh功能很强大,但是需要很强的算法基础,主要涉及三角函数等几何算法,那么如果实现山丘、海浪、银河系旋涡也有可能呢,这些仍然需要算法支持和不断的探索。