前言
我们之前的两篇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);
}
}