Android Canvas 球面贴图

前言

之前的文章中我们一直在构造球体,围绕的主线是给球体贴图,但是球体表面是凸起的,因此需要将二维平面做适当的旋转,这个难度其实很高。在这个过程中我做了很多常识,当然有些副产品,效果还是不错的。

后来在浏览博客时发现一篇《地球仪式分布的控件,球体控件》文章中成功实现了二维图片旋转,当然这篇博客作者已经很久没有更新了,无法得知具体的推导公式。

本篇最终效果预览

贴图的旋转角度

旋转公式,遗憾的是无法解释这个推导过程,不过这个在我绘制Canvas的绘制的时候,也是相当准确的。

java 复制代码
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));

绘制坑点

硬件加速问题

在绘制过程中其实踩了很多坑,其中一个是同一个Bitmap多次绘制图形之后在绘制到canvas上,最终绘制导致颜色都一样了

使用bitmap.eraseColor和drawColor都无法修复。

Canvas矩阵问题

Canvas 矩阵旋转导致全局放大,因此在Canvas上使用Matrix一定要小心。

透视问题

上一篇我们使用了透视除法,在这个过程中发现scale会关联很多东西,如颜色和大小,导致难以控制,因为 0< scale <2,因此本篇进行了简化,使其在0->1之间,不再做过多的透视,因为理论上scale本身就是透视的结果。

java 复制代码
// 透视除法,z轴向内的方向
float scale = (radius + point.z) / (2 * radius);
point.scale = Math.max(scale, 0.35f);

Camera旋转问题

在旋转的过程中发现,Camera.rotateX和Camera.rotateY的调用顺序会导致最终展示的差异,因此以一定要保持旋转的过程一致

java 复制代码
R(x) * R(y) * R(z)

核心逻辑

使用多个Bitmap分片绘制

java 复制代码
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);

    }
}

旋转

java 复制代码
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);
}

贴图

java 复制代码
//排序,先画背面的,再画正面的
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的投影。

全部代码

java 复制代码
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);
    }

}
相关推荐
anyup_前端梦工厂18 分钟前
Vuex 入门与实战
前端·javascript·vue.js
程序员阿鹏32 分钟前
ArrayList 与 LinkedList 的区别?
java·开发语言·后端·eclipse·intellij-idea
18你磊哥34 分钟前
java重点学习-JVM类加载器+垃圾回收
java·jvm
聂 可 以36 分钟前
在SpringBoot项目中利用Redission实现布隆过滤器(布隆过滤器的应用场景、布隆过滤器误判的情况、与位图相关的操作)
java·spring boot·redis
长安初雪1 小时前
Java客户端SpringDataRedis(RedisTemplate使用)
java·redis
你挚爱的强哥1 小时前
【sgCreateCallAPIFunctionParam】自定义小工具:敏捷开发→调用接口方法参数生成工具
前端·javascript·vue.js
米老鼠的摩托车日记1 小时前
【vue element-ui】关于删除按钮的提示框,可一键复制
前端·javascript·vue.js
aloha_7891 小时前
B站宋红康JAVA基础视频教程(chapter14数据结构与集合源码)
java·数据结构·spring boot·算法·spring cloud·mybatis
尘浮生1 小时前
Java项目实战II基于Java+Spring Boot+MySQL的洗衣店订单管理系统(开发文档+源码+数据库)
java·开发语言·数据库·spring boot·mysql·maven·intellij-idea
猿饵块2 小时前
cmake--get_filename_component
java·前端·c++