一、前言
使用Canvas绘制3D效果是老生常谈的问题,但是Canvas是2D坐标体系,实际上很难做到3D效果,即便是使用Camera,也只是投影效果,而真正的坐标体系本质上是不存在的,比如绕X、Y轴旋是不现实的事,Canvas只能绕Z轴旋转。另外还有3D投影固化为2D平面图,也会导致PathMeasure无法生效等问题,学完本篇,最终结论难道只能好好学open gl或者vulkan ?当然不是,经过一系列的探索,最终还是找到了新方法《Android Canvas 3D视图构建》,虽然本篇的方法是失败的,但也是成功之母,另外一些方法显然也可以用来绘制一些特殊效果。
二、效果
球体运动图
绕Y轴旋转的轨道图
星系图
三、问题处理
- 3D投影固化为2D平面
创建的Path如绕Y轴旋转的轨道图,本身应该绕Y轴旋转,那么作为观察者,理论上看不到整体旋转,实际效果如上图所展示的那样。因此我尝试了星系图,这个看似绕Y轴旋转了,实际上是把平面上的圆旋转了,如果旋转90,都看不到小圆本身。
- 观察点在球体中心导致视角错位
默认观察点是(0,0,0)点,这个会导致你看到的线条半径超过了球体半径,这个问题本篇克服了,原因是由于Camera的Z轴为0导致的,做以下修改即可,按球体半径一定距离即可
camera.setLocation(0, 0, -radius/2);
- PathMeasure 无效或者轨迹错位
原因是PathMeasure对变换后的Path没有建立起投影映射,因此使用PatheMeasure会有很多问题
- Canvas 无法绕X、Y旋转
通过投影方式,Canvas也无法旋转,当然可能我的方法也有问题
- 球体无法填充
球体表面没有合适的纹理贴图方法
四、案例代码
4.1 球体运动
public class MatrixPathView extends View {
private static final String TAG = "MatrixLearningView";
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
Matrix matrix = new Matrix();
Camera camera = new Camera();
private float[] vertex = new float[9];
private int mLineCount = 20;
private TimeInterpolator interpolator;
private float padding = 20F;
private Random random = new Random();
private List<CPath> paths = new ArrayList<CPath>();
private int row = 5;
private int col = 5;
private float rotate = 0f;
public MatrixPathView(Context context) {
this(context, null);
}
public MatrixPathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixPathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, 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);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width <= padding || height <= padding || mLineCount <= 0) {
return;
}
float radius = Math.min(width, height) / 2F - padding * 2;
int count = canvas.save();
vertex[0] = 1;
vertex[1] = 0;
vertex[2] = width / 2F;
vertex[3] = 0;
vertex[4] = 1;
vertex[5] = height / 2F;
vertex[6] = 0;
vertex[7] = 0;
vertex[8] = 1;
matrix.reset();
matrix.setValues(vertex);
canvas.concat(matrix);
float degree = 180F / row;
if (paths.isEmpty()) {
for (int i = 0; i < row; i++) {
// camera.save();
Matrix circleMatrix = new Matrix();
// camera.rotateX(i * degree);
// camera.setLocation(0,0,-radius/2F);
// camera.getMatrix(circleMatrix);
CPath path = new CPath(argb(random.nextFloat(),random.nextFloat(),random.nextFloat()),circleMatrix,i * degree,true);
//因为本身就在0,0点,因此不需要preTranslate和postTranslate
// path.addCircle(0, 0, radius, Path.Direction.CCW);
// path.transform(circleMatrix);
// camera.restore();
paths.add(path);
}
for (int i = 0; i < col; i++) {
camera.save();
Matrix circleMatrix = new Matrix();
CPath path = new CPath(argb(random.nextFloat(),random.nextFloat(),random.nextFloat()),circleMatrix,i * degree,false);
//因为本身就在0,0点,因此不需要preTranslate和postTranslare
// path.addCircle(0, 0, radius, Path.Direction.CCW);
// camera.rotateY(i * degree);
// camera.setLocation(0,0,-radius/2F); //注意Camera贴在0,0,0点看,会看到转转的东西很大
// camera.getMatrix(circleMatrix);
// path.transform(circleMatrix);
// camera.restore();
paths.add(path);
}
}
for (int i = 0; i < paths.size(); i++) {
CPath cPath = paths.get(i);
mCommonPaint.setColor(cPath.getColor());
Matrix pathMatrix = cPath.getMatrix();
pathMatrix.reset();
cPath.reset();
cPath.addCircle(0, 0, radius, Path.Direction.CCW);
camera.save();
if (cPath.isRotateX) {
camera.rotateX(i * degree + rotate);
}else{
camera.rotateY(i * degree + rotate);
}
camera.getMatrix(pathMatrix);
camera.setLocation(0, 0, -radius / 2F);
//注意Camera贴在0,0,0点看,那么坐标轴上的东西就会很大,所以眼睛远离屏幕
cPath.transform(pathMatrix);
camera.restore();
canvas.drawPath(cPath,mCommonPaint);
}
if (rotate > 360f) {
rotate = rotate - 360;
}
rotate += 5;
canvas.restoreToCount(count);
postInvalidateDelayed(32);
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setStrokeWidth(dp2px(1));
interpolator = new AccelerateDecelerateInterpolator();
}
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);
}
static class CPath extends Path{
private final Matrix matrix;
private final float degree;
private final boolean isRotateX;
int color;
public CPath(int color, Matrix matrix, float degree, boolean isRotateX) {
this.color = color;
this.matrix = matrix;
this.degree = degree;
this.isRotateX = isRotateX;
}
public Matrix getMatrix() {
return matrix;
}
public int getColor() {
return color;
}
}
}
4.2 绕Y轴旋转的轨道图
public class MatrixOribitPathView extends View {
private static final String TAG = "MatrixLearningView";
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
Matrix matrix = new Matrix();
Camera camera = new Camera();
private float[] vertex = new float[9];
private int mLineCount = 20;
private TimeInterpolator interpolator;
private float padding = 20F;
private Random random = new Random();
private List<CPath> paths = new ArrayList<CPath>();
private int num = 5;
private float rotate = 0f;
public MatrixOribitPathView(Context context) {
this(context, null);
}
public MatrixOribitPathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixOribitPathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, 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);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
Path path = new Path();
PathMeasure pathMeasure = new PathMeasure();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width <= padding || height <= padding || mLineCount <= 0) {
return;
}
float radius = Math.min(width, height) / 2F - padding * 2;
int count = canvas.save();
vertex[0] = 1;
vertex[1] = 0;
vertex[2] = width / 2F;
vertex[3] = 0;
vertex[4] = 1;
vertex[5] = height / 2F;
vertex[6] = 0;
vertex[7] = 0;
vertex[8] = 1;
matrix.reset();
matrix.setValues(vertex);
canvas.concat(matrix);
if (paths.isEmpty()) {
for (int i = 0; i < num; i++) {
Matrix circleMatrix = new Matrix();
CPath path = new CPath(argb(random.nextFloat(),random.nextFloat(),random.nextFloat()),circleMatrix);
paths.add(path);
}
}
path.reset();
float padding = radius / (num + 1);
for (int i = 0; i < paths.size(); i++) {
CPath cPath = paths.get(i);
mCommonPaint.setColor(cPath.getColor());
Matrix pathMatrix = cPath.getMatrix();
pathMatrix.reset();
cPath.reset();
cPath.addCircle(0, 0, radius - padding * i, Path.Direction.CCW);
camera.save();
camera.rotate(90 + 45,rotate,0);
camera.getMatrix(pathMatrix);
camera.setLocation(0, 0, -radius / 2F);
//注意Camera贴在0,0,0点看,那么坐标轴上的东西就会很大,所以眼睛远离屏幕
cPath.transform(pathMatrix);
camera.restore();
canvas.drawPath(cPath, mCommonPaint);
}
if (rotate > 360f) {
rotate = rotate - 360;
}
rotate += 5;
canvas.restoreToCount(count);
postInvalidateDelayed(32);
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setStrokeWidth(dp2px(1));
interpolator = new AccelerateDecelerateInterpolator();
}
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);
}
static class CPath extends Path{
private final Matrix matrix;
int color;
public CPath(int color, Matrix matrix) {
this.color = color;
this.matrix = matrix;
}
public Matrix getMatrix() {
return matrix;
}
public int getColor() {
return color;
}
@Override
public void reset() {
super.reset();
}
}
}
4.3 星系图
public class MatrixStarPathView extends View {
private static final String TAG = "MatrixLearningView";
private final DisplayMetrics mDM;
private TextPaint mCommonPaint;
Matrix matrix = new Matrix();
Camera camera = new Camera();
private float[] vertex = new float[9];
private int mLineCount = 20;
private TimeInterpolator interpolator;
private Random random = new Random();
private List<CPath> paths = new ArrayList<CPath>();
private int num = 5;
private float rotate = 0f;
public MatrixStarPathView(Context context) {
this(context, null);
}
public MatrixStarPathView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixStarPathView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, 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);
}
public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}
public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
Path path = new Path();
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width <= 10 || height <= 10 || mLineCount <= 0) {
return;
}
float radius = Math.min(width, height) / 2F - 20;
int count = canvas.save();
vertex[0] = 1;
vertex[1] = 0;
vertex[2] = width / 2F;
vertex[3] = 0;
vertex[4] = 1;
vertex[5] = height / 2F;
vertex[6] = 0;
vertex[7] = 0;
vertex[8] = 1;
matrix.reset();
matrix.setValues(vertex);
canvas.concat(matrix);
float degree = 180F / num;
if (paths.isEmpty()) {
for (int i = 0; i < num; i++) {
Matrix circleMatrix = new Matrix();
CPath path = new CPath(argb(random.nextFloat(),random.nextFloat(),random.nextFloat()),circleMatrix,i * degree,true);
paths.add(path);
}
}
path.reset();
float padding = radius / (num + 1);
for (int i = 0; i < paths.size(); i++) {
CPath cPath = paths.get(i);
mCommonPaint.setColor(cPath.getColor());
Matrix pathMatrix = cPath.getMatrix();
pathMatrix.reset();
cPath.reset();
float circleRadius = radius - padding * i;
cPath.addCircle(0, 0, radius - padding * i, Path.Direction.CCW);
cPath.addCircle((float) (circleRadius*Math.cos(Math.toRadians(rotate))), (float) (circleRadius*Math.sin(Math.toRadians(rotate))),10, Path.Direction.CCW);
camera.save();
camera.rotateX(90 - 45);
camera.getMatrix(pathMatrix);
// pathMatrix.postTranslate(0, padding * i);
camera.setLocation(0, 0, -radius / 2F);
//注意Camera贴在0,0,0点看,那么坐标轴上的东西就会很大,所以眼睛远离屏幕
cPath.transform(pathMatrix);
camera.restore();
canvas.drawPath(cPath, mCommonPaint);
}
if (rotate > 360f) {
rotate = rotate - 360;
}
rotate += 1;
canvas.restoreToCount(count);
postInvalidateDelayed(32);
}
private void initPaint() {
//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
mCommonPaint.setStrokeWidth(dp2px(1));
interpolator = new AccelerateDecelerateInterpolator();
}
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);
}
static class CPath extends Path{
private final Matrix matrix;
private final float degree;
private final boolean isRotateX;
int color;
public CPath(int color, Matrix matrix, float degree, boolean isRotateX) {
this.color = color;
this.matrix = matrix;
this.degree = degree;
this.isRotateX = isRotateX;
}
public Matrix getMatrix() {
return matrix;
}
public int getColor() {
return color;
}
}
}
五、总结
在Canvas 上绘制3D做简单的图片转是可以的,但是对于其他构图显然不太合适,虽然本篇大量使用了Path,但Path也有很多缺陷,其中之一是性能,其次是本篇说到的纹理贴图问题,无法在球体表面形成闭合空间(当然也可能是我的方法问题)。
意外收获:圆弧最大的旋转角度不能超过起始角,因此最大的旋转角度359.99983999