Android 3D球面贴图

1,863 阅读6分钟

前言

在任何计算机设备中,三维是不能存在的,你所能看到的3D图像,往往都是在2D平面上的投影,球体也一样。我们之前的文章,确立了球体的画法,那么,我们如何才能给球面贴上图片呢?

如果能画出地球仪,需要那些步骤呢?

显然,地球仪实现会更加复杂,,我们这里先在球面贴一些图像,至于如何铺满球面,我们后续有经验了再处理。

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

本篇最终效果预览

fire_86.gif

贴图的旋转角度

其实本篇最难的地方就是旋转公式的建模,我们知道,在计算机中,实际上不存在真正的3D图形,你所能看见的3D图形不过是3D模型在2D屏幕上的投影。

那么,如何计算沿着y轴的旋转公式呢?

至于如何推导增量公式,需要学习下空间集合和线性代数。

企业微信20240314-071222@2x.png

企业微信20240317-070820@2x.png

这张图是极坐标系的公式,我们要计算的是,经过球面的某一点的切面与各个轴的夹角,那么计算方式是有差别的。

在本篇,我们为了旋转贴图,需要计算平面与各个轴的夹角,原理是先求出在投影的长度,在计算出相应的角度。

float positionX = point.x;
float positionY = point.y ;
//求出y-z平面的投影长度
double Ryz = Math.sqrt(radius * radius - positionX * positionX); 
 //三位平面上绕x轴旋转其实点与y轴的夹角变化
float rotationX = -(float) (Math.asin(positionY / Ryz));
//三位平面上绕y轴旋转其实是点与x轴的夹角变化
float rotationY = -(float) (Math.asin(-positionX / radius));

上面,公式中为什么要求y-z平面的长度呢?

主要原因是,y点需要在y-z平面内,属于一个横截面,并不一定是y=0或者x=0的情况,因此,为了约束对x倾角,必须在约束的y-z半径范围内,否则会完全和球的半径一样,导致无法倾斜。

实际上,由y推理x也是可以推出来的,那种情况就需要计算x-z平面投影。

绘制坑点

硬件加速问题

在绘制过程中其实踩了很多坑,其中一个是同一个Bitmap多次绘制图形之后在绘制到canvas上,最终绘制导致颜色都一样了,为什么会这样呢?主要是因为开启了硬件加速之后,Bitmap 绘制指令会被优化,同时Bitmap引用都是一样的,就会造成奇怪的问题。

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

fire_87.gif

Canvas矩阵问题

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

透视问题

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

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

Camera旋转顺序问题

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

R(x) * R(y) * R(z)

核心逻辑

使用多个Bitmap分片绘制

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

    }
}

旋转,我们的旋转顺序一定要和Camera Matrix部分的旋转顺序一致。

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

贴图,这里其实可以绘制多张Bitmap,鉴于Bitmap 得找一些图片,我这里图省事,画成圆圈代替吧。

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

全部代码

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

}

参考

地球仪式分布的控件,球体控件