前言
在任何计算机设备中,三维是不能存在的,你所能看到的3D图像,往往都是在2D平面上的投影,球体也一样。我们之前的文章,确立了球体的画法,那么,我们如何才能给球面贴上图片呢?
如果能画出地球仪,需要那些步骤呢?
显然,地球仪实现会更加复杂,,我们这里先在球面贴一些图像,至于如何铺满球面,我们后续有经验了再处理。
之前的文章中我们一直在构造球体,围绕的主线是给球体贴图,但是球体表面是凸起的,因此需要将二维平面做适当的旋转,这个难度其实很高。在这个过程中我做了很多尝试,当然有些副产品,效果还是不错的。
本篇最终效果预览
贴图的旋转角度
其实本篇最难的地方就是旋转公式的建模,我们知道,在计算机中,实际上不存在真正的3D图形,你所能看见的3D图形不过是3D模型在2D屏幕上的投影。
那么,如何计算沿着y轴的旋转公式呢?
至于如何推导增量公式,需要学习下空间集合和线性代数。
这张图是极坐标系的公式,我们要计算的是,经过球面的某一点的切面与各个轴的夹角,那么计算方式是有差别的。
在本篇,我们为了旋转贴图,需要计算平面与各个轴的夹角,原理是先求出在投影的长度,在计算出相应的角度。
float positionX = point.x;
float positionY = point.y ;
//求出y-z平面的投影长度
double Ryz = Math.sqrt(radius * radius - positionX * positionX);
//三位平面上绕x轴旋转其实点与y轴的夹角变化
float rotationX = -(float) (Math.asin(positionY / Ryz));
//三位平面上绕y轴旋转其实是点与x轴的夹角变化
float rotationY = -(float) (Math.asin(-positionX / radius));
上面,公式中为什么要求y-z平面的长度呢?
主要原因是,y点需要在y-z平面内,属于一个横截面,并不一定是y=0或者x=0的情况,因此,为了约束对x倾角,必须在约束的y-z半径范围内,否则会完全和球的半径一样,导致无法倾斜。
实际上,由y推理x也是可以推出来的,那种情况就需要计算x-z平面投影。
绘制坑点
硬件加速问题
在绘制过程中其实踩了很多坑,其中一个是同一个Bitmap多次绘制图形之后在绘制到canvas上,最终绘制导致颜色都一样了,为什么会这样呢?主要是因为开启了硬件加速之后,Bitmap 绘制指令会被优化,同时Bitmap引用都是一样的,就会造成奇怪的问题。
使用bitmap.eraseColor和drawColor都无法修复。
Canvas矩阵问题
Canvas 矩阵旋转导致全局放大,因此在Canvas上使用Matrix一定要小心。
透视问题
上一篇我们使用了透视除法,在这个过程中发现scale会关联很多东西,如颜色和大小,导致难以控制,因为 0< scale <2,因此本篇进行了简化,使其在0->1之间,不再做过多的透视,因为理论上scale本身就是透视的结果。
// 透视除法,z轴向内的方向
float scale = (radius + point.z) / (2 * radius);
point.scale = Math.max(scale, 0.35f);
Camera旋转顺序问题
在旋转的过程中发现,Camera.rotateX和Camera.rotateY的调用顺序会导致最终展示的差异,因此以一定要保持旋转的过程一致
R(x) * R(y) * R(z)
核心逻辑
使用多个Bitmap分片绘制
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);
}
}
旋转,我们的旋转顺序一定要和Camera Matrix部分的旋转顺序一致。
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);
}
贴图,这里其实可以绘制多张Bitmap,鉴于Bitmap 得找一些图片,我这里图省事,画成圆圈代替吧。
//排序,先画背面的,再画正面的
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的投影。
全部代码
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);
}
}