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...

相关推荐
passer__jw7675 分钟前
【LeetCode】【算法】283. 移动零
数据结构·算法·leetcode
光影少年7 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_8 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891110 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾12 分钟前
前端基础-html-注册界面
前端·算法·html
Dragon Wu14 分钟前
前端 Canvas 绘画 总结
前端
顾北川_野16 分钟前
Android CALL关于电话音频和紧急电话设置和获取
android·音视频
CodeToGym19 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫20 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
顶呱呱程序20 分钟前
2-143 基于matlab-GUI的脉冲响应不变法实现音频滤波功能
算法·matlab·音视频·matlab-gui·音频滤波·脉冲响应不变法