Android 3D球面运动

前言

我们之前的两篇3D文章中,我们对3D的画法进行了一些探索,主要通过两种方式去构建3D效果,最终,我们找到了适合我们自己构建3D物体的方法。 《Android Canvas绘制3D视图探索》是踩坑篇,通过Path构建路径之后选择整体图形,发现事与愿违,Path 路径旋转后依然是坍塌的2D平面。如下图所示,我们需要穿过中心绕Y轴运动,结果整个屏幕都动了,另外由于点直接无法建立连接,导致外部无法形成闭合区域,因此也无法贴图。

接着我们《Android Canvas 3D视图构建》中终于找到了我们想要的方法,那就是通过数学模型动态连接Path的方式,先让每个点分布在3D物体的位点,当然点越多精度会越多,就像割圆术求圆周率一样。而且这种方式避免了点无法连接的问题,因此可以形成闭合曲面进行贴图。

球面运动公式

ini 复制代码
x =  mRadius * Math.cos(a) * Math.sin(b);
y =  mRadius * Math.sin(a) * Math.sin(b);
z =  mRadius * Math.cos(b);

其中, 角a为点在ab平面上的投影与x轴的夹角,b为点与x-y平面的夹角。

好了,现在就以画一个球围绕面旋转了

a = 0度, 绕Y轴

a = 90度 ,绕X轴

a = -45度,绕x-y平面夹角

b=90度 ,绕Z轴旋转

从上面三个图我们知道,其实这里的投影完全依赖z轴的运动,因此,3D是不存在的,你看到的3D只不过是2D平面的投影。

核心逻辑

为了大家体验,我把代码贴出来

核心逻辑当然是数学模型,但是绘制方法我们也要理解一下,在绘制的过程中我们需要球缩小。 之前的文章中,我们有提到,x/z和y/z来做透视投影,但这篇我们没用,主要原因是其中夹角的关系已经做了类似的换转,如果使用矩阵,那么这种透视除法必须手动做才行。

java 复制代码
    canvas.drawCircle(point.x, point.y, 50 + (point.z / radius) * 25 , mCommonPaint);

下面是比较完整的代码,当然公式的角度要转为圆周值

java 复制代码
double a = 0;
double b = 10;

@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) / 2.5f;
    int save = canvas.save();
    canvas.translate(width/2f,height/2f);

    point.x = (float) (radius * Math.cos(Math.toRadians(a)) * Math.sin(Math.toRadians(b)));
    point.y = (float) (radius * Math.sin(Math.toRadians(a)) * Math.sin(Math.toRadians(b)));
    point.z = (float) (radius * Math.cos(Math.toRadians(b)));

    canvas.drawCircle(point.x, point.y, 50 + (point.z / radius) * 25 , mCommonPaint);
    mCommonPaint.setStyle(Paint.Style.FILL);
    mCommonPaint.setColor(0x66ff9922);
    canvas.drawCircle(0,0,radius,mCommonPaint);
    mCommonPaint.setStyle(Paint.Style.FILL);
    mCommonPaint.setColor(0xFF2299ff);
    canvas.restoreToCount(save);

   // a += 1;
    b += 5;

    if(a > 360){
        a = a - 360;
    }
    if(b > 360){
        b = b - 360;
    }
    postInvalidateDelayed(32);

}


static class Point{
    private float Radius;
    private float x;
    private float y;
    private float z;
}

夹角思考

我们在旋转的过程中,角a和角b中有很多时间做圆周运动,圆周运动中有很多推导和公式。如三角函数值:360 + 旋转角度 = 旋转角度;三角函数之中,虽然角度象限不同,但值可能是一样的,另外物体只能绕中心点运动;那么,使用这种方式来确定方向以及不饶中心点呢,显然还需要借助更多的计算,有没有一眼就能直观的方式呢?

其实是有的,那就是使用矩阵,矩阵写对角线的数值的正负数很明确的就能看出点所在的大致区域,但是如何表示这个矩阵呢?当然,在欧拉等数学家的努力下,欧拉角的出现,让这个问题得到了解决。欧拉角可以和矩阵互相转换,优缺点互补,进行缩放平移、方向确定等。(注:当然还有其他表示方法,比如四元素、指数映射、矩阵)

欧拉角的含义: 欧拉角用了三个角度分量表示(a,b,c),每个角度用来表示绕X,Y,Z轴旋转的角度,和初始角度不同的是(a,b,c为增量角度,不是指定角度,因此不需要累加),我们无需要关注与平面的夹角,只关注旋转角,虽然靠想象依然很难定位方向,但是转换为矩阵结果就能清楚的知道方向。

实战

排列

我们首先定义初始位置 ,为什么要初始位置呢?看一下欧拉角的矩阵变换,其本质是依赖前值的,如果前值都是0,意味着都堆到了原点(0,0,0)位置。

java 复制代码
   //随机排列
   float alpha = random.nextFloat() * 360;
   float delta = random.nextFloat() * 180;  //靠正面

定义旋转角

我们在前面说过,欧拉角是增量角度旋转(依赖前值,在前值的基础上旋转),意味角度不需要累加。

ini 复制代码
double xr = Math.toRadians(5);  //绕x轴旋转
double yr = Math.toRadians(5);  //绕y轴旋转;
double zr = 0;  // 绕z轴旋转

旋转起来

当然我们使用三个角的乘积公式也是可以的,不过这里方便大家理解,就分为三步矩阵相乘。

ini 复制代码
float x = point.x;
float y = point.y;
float z = point.z;

//绕X轴旋转,乘以X轴的旋转矩阵
float rx1 = x;
float ry1 = (float) (y * Math.cos(xr) + z * -Math.sin(xr));
float rz1 = (float) (y * Math.sin(xr) + z * Math.cos(xr));

// 绕Y轴旋转,乘以Y轴的旋转矩阵
float rx2 = (float) (rx1 * Math.cos(yr) + rz1 * Math.sin(yr));
float ry2 = ry1;
float rz2 = (float) (rx1 * -Math.sin(yr) + rz1 * Math.cos(yr));

// 绕Z轴旋转,乘以Z轴的旋转矩阵
float rx3 = (float) (rx2 * Math.cos(zr) + ry2 * -Math.sin(zr));
float ry3 = (float) (rx2 * Math.sin(zr) + ry2 * Math.cos(zr));
float rz3 = rz2;


point.x = rx3;
point.y = ry3;
point.z = rz3;

预览一下

增加透视效果

因为在计算机设备中,无论是手机还是电脑,3D立面是不存在的,存在的是3D模型在2D平面的投影,因此为了更加逼真,我们让原理我们眼睛方向的物体缩小,颜色变浅,反之变大,颜色变深,我们使用透视除法,不过这里做些优化

scss 复制代码
// 透视除法,z轴向外
float scale = (2 * radius) / ((2 * radius) + rz3);
mCommonPaint.setColor(point.color);
if(scale > 1) {
    mCommonPaint.setAlpha(255);
}else{
    mCommonPaint.setAlpha((int) (scale * 255));
}

添加更多物体,绕X轴旋转

定义旋转角

ini 复制代码
double xr = Math.toRadians(5);  //绕x轴旋转
double yr = 0;  //绕y轴旋转;
double zr = 0;  // 绕z轴旋转

排列

arduino 复制代码
if(pointList.isEmpty()){

    int max = 20;

    for (int i = 0; i < max; i++) {

        //均匀排列
        float delta = (float) Math.acos(-1.0 + (2.0 * i - 1.0) / max);
        float alpha = (float) (Math.sqrt(max * Math.PI) * delta);

        //随机排列
      //  float alpha = random.nextFloat() * 360;
     //   float delta = random.nextFloat() * 180;  //靠正面

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

增加Shader效果

增加Shader 为了让大圆像太阳一样

ini 复制代码
if(shader == null){
    shader = new RadialGradient(0, 0, radius,
            new int[]{0xaaec5533, 0x77f9922, 0x11000000},
            new float[]{0.3f, 0.8f, 0.9f},
            Shader.TileMode.CLAMP);
}

多粒子效果预览

总结

到这里本篇就结束了,在本篇,我们可以有一个更加具体的方法去定义三维物体的运动,那就是欧拉角和矩阵,这是一种通用做法,包括在open gl的世界坐标系中,因此,本篇作为一个一篇3D模型构建篇,相比前2篇更加具体。

全部代码

java 复制代码
public class Smartian3DView extends View {

    private TextPaint mCommonPaint;
    private  DisplayMetrics mDM;


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

    public Smartian3DView(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);

    }


    double xr = Math.toRadians(5);  //绕x轴旋转
    double yr = 0;  //绕y轴旋转;
    double zr = 0;

    private List<Point> pointList = new ArrayList<>();

    private Random random = new Random();

    private Shader shader = null;

    @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++) {

                //均匀排列
                float delta = (float) Math.acos(-1.0 + (2.0 * i - 1.0) / max);
                float alpha = (float) (Math.sqrt(max * Math.PI) * delta);

                //随机排列
              //  float alpha = random.nextFloat() * 360;
             //   float delta = random.nextFloat() * 180;  //靠正面

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

        for (int i = 0; i < pointList.size(); i++) {

            Point point = pointList.get(i);
            float x = point.x;
            float y = point.y;
            float z = point.z;

            //绕X轴旋转,乘以X轴的旋转矩阵
            float rx1 = x;
            float ry1 = (float) (y * Math.cos(xr) + z * -Math.sin(xr));
            float rz1 = (float) (y * Math.sin(xr) + z * Math.cos(xr));

            // 绕Y轴旋转,乘以Y轴的旋转矩阵
            float rx2 = (float) (rx1 * Math.cos(yr) + rz1 * Math.sin(yr));
            float ry2 = ry1;
            float rz2 = (float) (rx1 * -Math.sin(yr) + rz1 * Math.cos(yr));

            // 绕Z轴旋转,乘以Z轴的旋转矩阵
            float rx3 = (float) (rx2 * Math.cos(zr) + ry2 * -Math.sin(zr));
            float ry3 = (float) (rx2 * Math.sin(zr) + ry2 * Math.cos(zr));
            float rz3 = rz2;


            point.x = rx3;
            point.y = ry3;
            point.z = rz3;

            // 透视除法,z轴向外
            float scale = (2 * radius) / ((2 * radius) + rz3);
            mCommonPaint.setColor(point.color);
            if(scale > 1) {
                mCommonPaint.setAlpha(255);
            }else{
                mCommonPaint.setAlpha((int) (scale * 255));
            }
            canvas.drawCircle(point.x * scale, point.y * scale, 5 + 25 * scale, mCommonPaint);

        }
        mCommonPaint.setAlpha(255);
        if(shader == null){
            shader = new RadialGradient(0, 0, radius,
                    new int[]{0xaaec5533, 0x77f9922, 0x11000000},
                    new float[]{0.3f, 0.8f, 0.9f},
                    Shader.TileMode.CLAMP);
        }
        mCommonPaint.setShader(shader);
        mCommonPaint.setStyle(Paint.Style.FILL);
        canvas.drawCircle(0,0,radius,mCommonPaint);
        mCommonPaint.setShader(null);
        canvas.restoreToCount(save);

        postInvalidateDelayed(32);

    }


    static class Point{
        private int color;
        private float x;
        private float y;
        private float z;
    }


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

}

参考

github.com/misakuo/3dT...

相关推荐
王解1 分钟前
一篇文章读懂 Prettier CLI 命令:从基础到进阶 (3)
前端·perttier
乐闻x7 分钟前
最佳实践:如何在 Vue.js 项目中使用 Jest 进行单元测试
前端·vue.js·单元测试
檀越剑指大厂21 分钟前
【Python系列】异步 Web 服务器
服务器·前端·python
我是Superman丶23 分钟前
【前端】js vue 屏蔽BackSpace键删除键导致页面后退的方法
开发语言·前端·javascript
Hello Dam25 分钟前
基于 Spring Boot 实现图片的服务器本地存储及前端回显
服务器·前端·spring boot
小仓桑26 分钟前
利用 Vue 组合式 API 与 requestAnimationFrame 优化大量元素渲染
前端·javascript·vue.js
Hacker_xingchen27 分钟前
Web 学习笔记 - 网络安全
前端·笔记·学习
天海奈奈27 分钟前
前端应用界面的展示与优化(记录)
前端
qq_4856689938 分钟前
算法习题--蓝桥杯
算法·职场和发展·蓝桥杯
waves浪游41 分钟前
类和对象(中)
c语言·开发语言·数据结构·c++·算法·链表