Android Canvas 3D视图构建

1,296 阅读3分钟

一、前言

之前写过一篇《Android Canvas 3D视图探索》,在那篇内容中,我们尝试使用Camera去构建3D视觉,但是由于Canvas 是2D体系,必然3D只能投影到2D平面,且最终坍塌成平面图,有点像三体世界的降维打击(当然我的方法可能也有问题),因此本篇将抛弃Camera,通过2D平面去构建三维图形,而不是直接使用Camera构建三维图形。

当然你可能觉得(x,y,z)坐标怎么表示,我们可以将其想象成(x,y,1),z轴用来做透视除法,类似open gl中的(x,y,z,w) ,其中w用来做透视除法,当然本篇也没有用到z轴,主要是时间关系,懒得处理原理屏幕的画面大小了。

透视除法: 类似(x,y,z)转嫁为(x/z,y/z)的一种投影

构建原理:

  • 二维平面看三维平面是动态的,二维无法向三位形成闭合Path,因此我们采用运动方程实现点与点之间的连接,类似open gl 中的三角扇。

  • 图元组合,主要原因是只有通过这种方式才能生成闭合Path,当然本篇没有将点连成线,而是写了一些数字

效果图

很炫吧,当然我觉得你可能喜欢下面的

或者下面的

当然,上面的图片我们没有做透视除法,因此原理屏幕的元素稍微有些大,但并不影响你拿去做扩展,比如开发循环View。

二、模型

2.1  关于矩阵

我们知道2D平面的矩阵是

MSCALE_X,MSKEW_X,MTRANS_X,
MSKEW_Y,MSCALE_Y,MTRANS_Y,
MPERSP_0,MPERSP_2,MPERSP_2

实际上MSCALE_X和MSCALE_Y 替换成 R*sinθ或者R*cosθ 不就意味着可以旋转了么 ? 答案是当然可以。

R - 半径

θ - 二维平面中点与x轴正方向的夹角

二维平面只需要知道半径和一个角就能计算出某个点的坐标,三维需要半径和2个角才能计算出三维空间上某个点的坐标。

二维:

x = R*sinθ

y = R * cosθ

三维:

x = R*sinα*cosθ

y = R*sinθ*sinα

z= R*cosθ

但是貌似和三维旋转矩阵没有什么关系?

其实这是点在三维平面点的变换方式,实际上并没有违反任何原则,点依然可以按照坐标表示,但矩阵的存在会让旋转更加方便计算。

下面是使用矩阵表示点的方式最终乘积表示方法,先与x轴旋转矩阵相乘,在与y轴旋转矩阵相乘,最后与z轴旋转矩阵相乘。

Y表示与y轴方向的夹角,X表示与x轴方向的夹角,Z表示与z轴方向的夹角。

float x = x * mCosY + (y * mSinX + z * mCosX) * mSinY;
float y = y * mCosX + z * -mSinX;
float z = x * -mSinY + (y * mSinX + z * mCosX) * mCosY;

知道R半径和夹角的情况下,上面的公式理论上和下面的是等价的

x = R * sinα * cosθ
y = R * sinθ * sinα
z = R * cosθ

我们绘图中两种都可以使用,但是矩阵的表示显然复杂的多,那为什么会存在这种复杂的方式呢?其实主要是为了利于gpu计算。

当然,本文不会用到上述数学模型,本篇核心点是构建一个简单绕y轴旋转的动效,那结果就是

x = R* cosθz=1y= N + offsetR * cosp 

offsetR 是自行计算出的半径,cosp 是为了产生倾斜效果加的,换成sinp也是可以,N是y轴的偏移高度,因此本篇其实并没有涉及到球体,也只是简单的三维效果,和球体不是一回事。

这里不建议使用Camera,比如你知道Camera.rotateX创建的矩阵Z分量没有任何变化,具体我也不知道为什么,知道的可能在评论区留言。

2.2 矩阵转换为公式

我们使用矩阵与向量相乘操作还是太麻烦,那能不能有简单的方法呢?

如果绕Y轴旋转,只需要修改X分量即可

  • Y = N (N 为定值) +offsetR * cosp

  • X = Radius * sinθ 或者Radius * cosθ

反过来绕X轴旋转,只需要修改Y分量

  • X = N (N 为定值) + offsetR * cosp

  • Y = Radius * sinθ或者 Radius * cosθ

进一步,为了让原理屏幕的有一定的夹角,N值可以乘以Cosθ

再进一步,为了让原理屏幕的变小,X/Z,Y/Z即可,当然本篇没有做这个。

三、代码实现

核心代码

        float degree = 360f;
        char[] text = "1234567890".toCharArray();
        int num = text.length;

        //5组平分半径
        int group = 5;
        float v = radius / group;  //y位置
        // y = R - v*i
        //  Math.sqrt(radius*radius - v*v)  推出  x轴小圆 半径


        for (int k = 0; k < group; k++) {
            for (int i = 0; i < num; i++) {
                float ty = radius - (radius / group) * (k + 1);  //平分最大圆半径
                float r = (float) Math.sqrt(radius * radius - ty * ty);  //勾股定理求出小圆的半径
                //cy是Y坐标,50是给的一定的倾角,看起来更加动态
                 float cy = (float) (-ty - 50 * Math.cos(Math.toRadians(degree / num * i + rotate)));
/               //通过半径计算出平面小圆的在旋转角度的半径
                 float cx = (float) (r * Math.sin(Math.toRadians(degree / num * i + rotate)));
                canvas.drawText(text, num - i - 1, 1, cx, cy, mCommonPaint);
                if (k < (group - 1)) {
                    canvas.drawText(text, num - i - 1, 1, cx, -cy, mCommonPaint);
                }
            }
        }

全部代码

public class MatrixCircleView extends BaseView {
    private static final String TAG = "MatrixLearningView";
    private final DisplayMetrics mDM;
    private final Bitmap mBitmap;
    private TextPaint mCommonPaint;
    Matrix matrix = new Matrix();
    private float[] vertex = new float[9];
    private float padding = 40F;

    private Random random = new Random();

    private float rotate = 0f;

    public MatrixCircleView(Context context) {
        this(context, null);
    }

    public MatrixCircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MatrixCircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.mm_010);
    }

    @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);
    }

    @Override
    protected void init(Context context) {

    }


    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);
    }

    RectF bitmapRect = new RectF();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawCoordinate(canvas);
        int width = getWidth();
        int height = getHeight();
        if (width <= padding || height <= padding) {
            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 = 360f;
        char[] text = "1234567890".toCharArray();
        int num = text.length;

        //5组平分半径
        int group = 5;
        float v = radius / group;  //y位置
        // y = R - v*i
        //  Math.sqrt(radius*radius - v*v)  = >  x 半径


        for (int k = 0; k < group; k++) {
            for (int i = 0; i < num; i++) {
                float ty = radius - (radius / group) * (k + 1);
                float r = (float) Math.sqrt(radius * radius - ty * ty);  //勾股定理求出小圆的半径
                float cy = (float) (-ty - 50 * Math.cos(Math.toRadians(degree / num * i + rotate)));
                float cx = (float) (r * Math.sin(Math.toRadians(degree / num * i + rotate)));
                canvas.drawText(text, num - i - 1, 1, cx, cy, mCommonPaint);
                if (k < (group - 1)) {
                    canvas.drawText(text, num - i - 1, 1, cx, -cy, mCommonPaint);
                }
            }
        }

//        下面是画图片的
//
//        float ty = 0;
//        float r = (float) Math.sqrt(radius * radius - ty * ty);  //勾股定理求出小圆的半径
//        for (int i = 0; i < num; i++) {
//            float cy = (float) (-ty - 90 * Math.cos(Math.toRadians(degree / num * i + rotate)));
//            float cx = (float) (r * Math.sin(Math.toRadians(degree / num * i + rotate)));
//            bitmapRect.set(cx - 50, cy - 50, cx + 50, cy + 50);
//            canvas.drawBitmap(mBitmap,null,bitmapRect,mCommonPaint);
//        }

        //下面是画辅助线的

        //     mCommonPaint.setStyle(Paint.Style.STROKE);
        //   canvas.drawCircle(0,0,radius,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.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        mCommonPaint.setStrokeWidth(dp2px(1));
        mCommonPaint.setTextSize(sp2px(12));
        mCommonPaint.setColor(argb(random.nextFloat(), random.nextFloat(), random.nextFloat()));
    }


    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);
    }

}

本篇同样也是探索篇,后续有时间我们自行实现一个球体。

四、总结

本篇到这里基本结束了,这是个骨架代码,后续可以衍生出更多效果,当然本次是X轴旋转,Z轴旋转没有实现,不过原理是相同的。

衍生效果:

  • 旋转木马

  • 旋转相册

  • 地球

  • 无限循环菜单

  • 裸眼3D

  • css动画

衍生效果大家可以自行实现。