前言
之前的文章中我们一直在构造球体,围绕的主线是给球体贴图,但是球体表面是凸起的,因此需要将二维平面做适当的旋转,这个难度其实很高。在这个过程中我做了很多常识,当然有些副产品,效果还是不错的。
后来在浏览博客时发现一篇《地球仪式分布的控件,球体控件》文章中成功实现了二维图片旋转,当然这篇博客作者已经很久没有更新了,无法得知具体的推导公式。
本篇最终效果预览
贴图的旋转角度
旋转公式,遗憾的是无法解释这个推导过程,不过这个在我绘制Canvas的绘制的时候,也是相当准确的。
java
float positionX = point.x;
float positionY = point.y ;
double rx = Math.sqrt(radius * radius - positionX * positionX);
float rotationX = -(float) (Math.asin(positionY / rx));
float rotationY = -(float) (Math.asin(-positionX / radius));
绘制坑点
硬件加速问题
在绘制过程中其实踩了很多坑,其中一个是同一个Bitmap多次绘制图形之后在绘制到canvas上,最终绘制导致颜色都一样了
使用bitmap.eraseColor和drawColor都无法修复。
Canvas矩阵问题
Canvas 矩阵旋转导致全局放大,因此在Canvas上使用Matrix一定要小心。
透视问题
上一篇我们使用了透视除法,在这个过程中发现scale会关联很多东西,如颜色和大小,导致难以控制,因为 0< scale <2,因此本篇进行了简化,使其在0->1之间,不再做过多的透视,因为理论上scale本身就是透视的结果。
java
// 透视除法,z轴向内的方向
float scale = (radius + point.z) / (2 * radius);
point.scale = Math.max(scale, 0.35f);
Camera旋转问题
在旋转的过程中发现,Camera.rotateX和Camera.rotateY的调用顺序会导致最终展示的差异,因此以一定要保持旋转的过程一致
java
R(x) * R(y) * R(z)
核心逻辑
使用多个Bitmap分片绘制
java
private List<Bitmap> bitmaps = new ArrayList<>();
private List<Bitmap> bitmaps = new ArrayList<>();
if (pointList.isEmpty()) {
int max = 20;
for (int i = 0; i < max; i++) {
//均匀排列
double v = -1.0 + (2.0 * i - 1.0) / max;
if (v < -1.0) {
v = 1.0f;
}
float delta = (float) Math.acos(v);
float alpha = (float) (Math.sqrt(max * Math.PI) * delta);
Point point = new Point();
point.x = (float) (radius * Math.cos(alpha) * Math.sin(delta));
point.y = (float) (radius * Math.sin(alpha) * Math.sin(delta));
point.z = (float) (radius * Math.cos(delta));
point.color = argb(random.nextFloat(), random.nextFloat(), random.nextFloat());
pointList.add(point);
Bitmap bitmap = Bitmap.createBitmap((int) (radius/2f), (int) (radius/2), Bitmap.Config.ARGB_8888);
bitmaps.add(bitmap);
}
}
旋转
java
for (int i = 0; i < pointList.size(); i++) {
Point point = pointList.get(i);
rotateX(point,xr);
rotateY(point,yr);
rotateZ(point,zr);
// 透视除法,z轴向内的方向
float scale = (radius + point.z) / (2 * radius);
point.scale = Math.max(scale, 0.35f);
}
贴图
java
//排序,先画背面的,再画正面的
Collections.sort(pointList, comparator);
for (int i = 0; i < pointList.size(); i++) {
int saveCount = canvas.save();
Point point = pointList.get(i);
Canvas bitmapCanvas = new Canvas(bitmaps.get(i));
int saveBitmapCount = bitmapCanvas.save();
bitmaps.get(i).eraseColor(Color.TRANSPARENT);
mCommonPaint.setARGB((int) (255 * point.scale), Color.red(point.color),Color.green(point.color),Color.blue(point.color));
float circleR = Math.min(bitmaps.get(i).getWidth()/2f,bitmaps.get(i).getHeight()/2f)* point.scale;
bitmapCanvas.drawCircle(bitmaps.get(i).getWidth()/2f,bitmaps.get(i).getHeight()/2f,circleR ,mCommonPaint);
bitmapCanvas.restoreToCount(saveBitmapCount);
float positionX = point.x;
float positionY = point.y ;
double rx = Math.sqrt(radius * radius - positionX * positionX);
float rotationX = -(float) (Math.asin(positionY / rx));
float rotationY = -(float) (Math.asin(-positionX / radius));
matrix.reset();
camera.save();
//先旋转X,再旋转Y,顺序不能变
camera.rotateX((float) Math.toDegrees(rotationX));
camera.rotateY((float) Math.toDegrees(rotationY));
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-bitmaps.get(i).getWidth()/2f, - bitmaps.get(i).getWidth()/2f);
matrix.postTranslate(point.x , point.y );
// 旋转单位矩阵,中心点为图片中心
canvas.drawBitmap(bitmaps.get(i),matrix,mCommonPaint);
canvas.restoreToCount(saveCount);
}
总结
本篇到这里就结束了,其实3D在Canvas上实现还是挺有难度的,不过,这个也是学习3D的一个过程,总结就一句话:计算机中不存在3D,3D不过是2D的投影。
全部代码
java
public class Smartian3D3View extends View {
private TextPaint mCommonPaint;
private DisplayMetrics mDM;
private Matrix matrix = new Matrix();
private Camera camera = new Camera();
double xr = Math.toRadians(5f); //绕x轴旋转
double yr = 0; //绕y轴旋转;
double zr = 0;
private List<Point> pointList = new ArrayList<>();
private Random random = new Random();
private List<Bitmap> bitmaps = new ArrayList<>();
public Smartian3D3View(Context context) {
this(context, null);
}
public Smartian3D3View(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
private void initPaint() {
mDM = getResources().getDisplayMetrics();
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setFilterBitmap(true);
mCommonPaint.setDither(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
if (width < 1) return;
int height = getHeight();
if (height < 1) return;
float radius = Math.min(width, height) / 3f;
int save = canvas.save();
canvas.translate(width / 2f, height / 2f);
if (pointList.isEmpty()) {
int max = 20;
for (int i = 0; i < max; i++) {
//均匀排列
double v = -1.0 + (2.0 * i - 1.0) / max;
if (v < -1.0) {
v = 1.0f;
}
float delta = (float) Math.acos(v);
float alpha = (float) (Math.sqrt(max * Math.PI) * delta);
Point point = new Point();
point.x = (float) (radius * Math.cos(alpha) * Math.sin(delta));
point.y = (float) (radius * Math.sin(alpha) * Math.sin(delta));
point.z = (float) (radius * Math.cos(delta));
point.color = argb(random.nextFloat(), random.nextFloat(), random.nextFloat());
pointList.add(point);
Bitmap bitmap = Bitmap.createBitmap((int) (radius/2f), (int) (radius/2), Bitmap.Config.ARGB_8888);
bitmaps.add(bitmap);
}
}
for (int i = 0; i < pointList.size(); i++) {
Point point = pointList.get(i);
rotateX(point,xr);
rotateY(point,yr);
rotateZ(point,zr);
// 透视除法,z轴向内的方向
float scale = (radius + point.z) / (2 * radius);
point.scale = Math.max(scale, 0.35f);
}
mCommonPaint.setStyle(Paint.Style.STROKE);
mCommonPaint.setColor(Color.GRAY);
canvas.drawCircle(0,0,radius,mCommonPaint);
mCommonPaint.setStyle(Paint.Style.FILL);
//排序,先画背面的,再画正面的
Collections.sort(pointList, comparator);
for (int i = 0; i < pointList.size(); i++) {
int saveCount = canvas.save();
Point point = pointList.get(i);
Canvas bitmapCanvas = new Canvas(bitmaps.get(i));
int saveBitmapCount = bitmapCanvas.save();
bitmaps.get(i).eraseColor(Color.TRANSPARENT);
mCommonPaint.setARGB((int) (255 * point.scale), Color.red(point.color),Color.green(point.color),Color.blue(point.color));
float circleR = Math.min(bitmaps.get(i).getWidth()/2f,bitmaps.get(i).getHeight()/2f)* point.scale;
bitmapCanvas.drawCircle(bitmaps.get(i).getWidth()/2f,bitmaps.get(i).getHeight()/2f,circleR ,mCommonPaint);
bitmapCanvas.restoreToCount(saveBitmapCount);
float positionX = point.x;
float positionY = point.y ;
double rx = Math.sqrt(radius * radius - positionX * positionX);
float rotationX = -(float) (Math.asin(positionY / rx));
float rotationY = -(float) (Math.asin(-positionX / radius));
matrix.reset();
camera.save();
//先旋转X,再旋转Y,顺序不能变
camera.rotateX((float) Math.toDegrees(rotationX));
camera.rotateY((float) Math.toDegrees(rotationY));
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-bitmaps.get(i).getWidth()/2f, - bitmaps.get(i).getWidth()/2f);
matrix.postTranslate(point.x , point.y );
// 旋转单位矩阵,中心点为图片中心
canvas.drawBitmap(bitmaps.get(i),matrix,mCommonPaint);
canvas.restoreToCount(saveCount);
}
canvas.restoreToCount(save);
postInvalidateDelayed(32);
}
private void rotateZ(Point point, double zr) {
// 绕Z轴旋转,乘以Z轴的旋转矩阵
float x = point.x;
float y = point.y;
float z = point.z;
point.x = (float) (x * Math.cos(zr) + y * -Math.sin(zr));
point.y = (float) (x * Math.sin(zr) + y * Math.cos(zr));
point.z = z;
}
private void rotateY(Point point, double yr) {
//绕Y轴旋转,乘以Y轴的旋转矩阵
float x = point.x;
float y = point.y;
float z = point.z;
point.x = (float) (x * Math.cos(yr) + z * Math.sin(yr));
point.y = y;
point.z = (float) (x * -Math.sin(yr) + z * Math.cos(yr));
}
private void rotateX(Point point, double xr) {
//绕X轴旋转,乘以X轴的旋转矩阵
float x = point.x;
float y = point.y;
float z = point.z;
point.x = x;
point.y = (float) (y * Math.cos(xr) + z * -Math.sin(xr));
point.z = (float) (y * Math.sin(xr) + z * Math.cos(xr));
}
Comparator comparator = new Comparator<Point>() {
@Override
public int compare(Point left, Point right) {
if (left.scale - right.scale > 0) {
return 1;
}
if (left.scale == right.scale) {
return 0;
}
return -1;
}
};
static class Point {
private int color;
private float x;
private float y;
private float z;
private float scale = 1f;
}
public static int argb(float red, float green, float blue) {
return ((int) (1 * 255.0f + 0.5f) << 24) |
((int) (red * 255.0f + 0.5f) << 16) |
((int) (green * 255.0f + 0.5f) << 8) |
(int) (blue * 255.0f + 0.5f);
}
}