基于Java2D和Java3D的图形编辑系统
摘 要
基于本学期计算机图形学课程的理解以及课外查找的资料内容,实现了一个基于Java2D及Java3D的图形编辑系统,可以供用户实时交互。基本功能包括二维图形的输入、编辑(裁剪+二维变换)、图像存储以及三维图形(.off文件)的载入。应用技术内容包括各个二维图元的生成算法、变换算法、图像裁剪算法、off文件读取算法以及Java2D/3D编程与API应用技术。整个工程遵循面向对象的设计原则,开发过程具有典型性。文档中注明的[x]在文档末尾,是该算法或过程参考的内容。
关键词:面向对象;Java2D/3D;二维图形;三维图形;图形化系统;交互
1 引言
计算机图形学研究关于计算机图形对象的建模、处理与绘制等方面的理论和技术。其基本目标是:构建图形对象的虚拟世界,并按特定视角将虚拟模型的场景载图形设备上绘制出来。
图形系统一般由建模组件和绘制组件两大部分组成,建模组件负责虚拟世界模型的构建,绘制组件完成场景的绘制。本次工程的核心也就是完成各类图形的建模并通过合适的绘制组件使其在屏幕上输出出来。
图形编程几乎在计算机体系结构的每个层次都留下了自己的身影。它的大致发展趋势是,从平台相关的底层方法向高层抽象的可移植环境发展。所包含的内容也远大于本工程的范畴。不过,可以从本工程开始,窥见一些基础的图形编程理念。
2 工程结构概述
总工程分为两个大方面:二维图形与三维图形。能力有限,并没有能够将这两方面功能结合在一个程序中,下面也将分两部分来介绍工程结构。
2.1 二维图形工程
工程UML框架如下:
介绍工程所包含的各个类:
- MyItem及形如MyXX的它的子类:定义了所需要绘制的各类二维图形的基本建模(也就是绘制算法)
- CutFrame类:定义了裁剪边框的建模,实际上裁剪边框是一个四边形,它的实现部分继承了四边形图元
- BoundaryFill类:定义了区域填充算法的建模
- TestItem类:程序Main函数所在类,定义了GUI框架的显示以及各个UI控件与类之间的响应,包含了各个用户可使用的按钮的接口实现,也实现了鼠标监听事件,是工程的主类
- Rotate接口:用于实现图元的旋转操作
- Contain接口:用于判断某个点是否在图元内部(配合实现选中图元操作)
- Zoom接口:用于实现图元的缩放操作
- Construct接口:用于实现图元的基本建模(绘制图形)
一个基本的操作流程及其背后的函数调用如下:
系统的主要类TestItem介绍:
主要部分为鼠标监听事件,它统筹管理了所有的事件,用一个pressIndex来管理系统的状态机。
//5种状态:0:无任何操作 1:选中图元 2:选中图元端点 3:开始画图 4: 填充区域
int pressIndex = 0;
下面是系统的状态转换图:
2.2 三维图形工程
三维图形加载实现在Load3D类中,类包括了一个简单的GUI框架以及一个Button用来链接加载文件的方法,调用该方法成功后,会在屏幕中显示三维模型。
一个基本的操作流程及其背后的函数调用如下:
3 主要原理及测试结果展示
3.1 直线生成算法
直线生成采用中点画线算法[1],具体算法过程如下:
对于一条直线,可以从鼠标的操作中获取到直线的两个端点,从而计算出直线的斜率k。
对于k的取值,我做出如下讨论:
- k趋向于无穷时
- k = 0时
- 0 < k < 1时(典例)
- 其余的k值情况
前两种情况下不作赘述,主要研究后两种取值。
以0 < k < 1为例,画线算法的思想如下:
从直线方程(隐函数)F(x,y)入手
F(x,y)=ax+by+c.
由
y = (y1-y0)/(x1-x0)•x + C(C为常量)
联合上式可以解得:
a = y0 - y1, b = x1 - x0, c = x0y1 - x1y0.
直线上方的点将满足F(x, y) > 0, 直线下方的点将满足F(x, y) < 0。
对于0 < k < 1的直线,情况如下图:
起点为(xi, yi)时,Q表示直线上的点,M表示下一格像素的中点。
将M点坐标代入并将求得的F(x, y)的值记为d。
求得d > 0时,表示M在直线上方,下一个画的点应选择为下方的P1点。
此时再往下求下一个点的F(x, y)值,记为d1.
则
d1 = F(xi + 2, yi + 0.5) = F(xi + 1, yi + 0.5) = d + a
d的增量det1为a.
反之d < 0时,表示M在直线下方,下一个画的点应选择为上方的P2点。
此时
d1 = F(xi + 2, yi + 1.5) = d + a + b
d的增量det2为a + b。
又考虑
d0 = F(x0 + 1, y0 + 0.5) = a + 0.5b
为满足整型要求,取d初值为2a + b,增量
det1 = 2a, det2 = 2(a + b)
代码如下:
if(k > 0 && k <= 1){
d = 2 * a + b;
det1 = 2 * a;
det2 = 2 * (a + b);
while(x < endx){
if(d < 0){
x++; y++;
d += det2;
}else{
x++; d += det1;
}
shape.lineTo(x, y);
}
}
再来考虑剩下的情况。如下图:
可以将直线所在坐标区域分为8个部分。实际上,上述算法只画出了区域1内的直线。
不过有了0 < k < 1的情况,其余情况都可以比较轻松地推导得出。
首先,为方便起见,本算法中规定了endx必为x0、x1中较大的一方,从而只需实现第一、第四象限内(即1,2,7, 8区域)的直线处理算法即可。
区域2:k > 1
可求得此时
d = a + 2b, det1 = 2b, det2 = 2 (a + b)
else if(k > 1){
d = a + 2 * b;
det1 = 2 * b;
det2 = 2 * (a + b);
while(y < endy){
if(d < 0){
y++; d += det1;
}else{
x++; y++; d += det2;
}
shape.lineTo(x, y);
}
}
区域8:-1 < k < 1
可求得此时
d = 2a - b, det1 = 2b, det2 = 2(a + b)
else if(k >= -1 && k <= 0){
d = 2 * a - b;
det1 = 2 * a;
det2 = 2 * (a - b);
while(x < endx){
if(d < 0){
x++; d += det1;
}else{
x++; y--; d += det2;
}
shape.lineTo(x, y);
}
}
区域7:k < -1
可求得此时
d = a - 2b, det1 = -2b, det2 = 2(a - b)
else if(k < -1){
d = a - 2 * b;
det1 = (-b) * 2;
det2 = 2 * (a - b);
while(y > endy){
if(d < 0){
x++; y--; d += det2;
}else{
y--; d += det1;
}
shape.lineTo(x, y);
}
}
下面测试四种情况下直线生成的正确性(展示的是用已实现的保存画布接口存下的画布,逐步添加四条不同直线):
k > 1
0 < k < 1
-1 < k < 0
k < -1
测试四种情况均正常。
除此以外,直线是可以通过端点来进行编辑操作的,技术依赖于鼠标监听事件中实现的实时重绘。
一个拖动端点来改变直线的测试如下:
拖动右侧端点至新的位置:
测试结果正常。
3.2 圆、椭圆生成算法
圆的绘制采用中点圆算法[2],具体过程如下:
对于一个已知圆,它的参量可以决定出圆上的点满足方程f(x, y) = 0。
在判断下一个画的点的位置时,可能的两个点的中点坐标代入f(x, y)求值,记为P。
- 若P≥0,则表示中点在圆外,应选择靠内侧的点
- 若P<0,则表示中点在圆内,应选择靠外侧的点
如下图所示:
以此类推,更新P的值。由于圆的对称性,只需迭代至八分之一圆周,将其余坐标通过对称操作得到即可。
具体算法如下:
- P(0) = 5/4 - r
- 求P(k) = f(x + 1, y - 1/2)
- 若P(k)<0,取下一个点为(x + 1, y),有递推公式:P(k+1) = P(k) + 2x + 3,重复步骤2直至x > y
- 若P(k)≥0,取下一个点为(x + 1, y - 1),有递推公式:P(k + 1) = P(k) + 2x -2y + 3,重复步骤2直至x > y
算法伪代码(参量已做过整数化处理,适应像素为整数):
int p = 1 - r; int px = 3, py = 5 - 2 * r;//p即p(k),px,py分别对应p不同取值下的增量
while(x <= y){
if(p < 0){
p += px;
px += 2; py += 2;
x++;
//求8个对称点
}else if(p >= 0){
p += py;
px += 2; py += 4;
x++; y--;
//求8个对称点
}
}
此外,需要注意的是,我的算法根据鼠标移动来得到初始画圆的两个端点,所以需要加一步判断两端点的关系,将x坐标较小的一方作为起始点,如下。
if(a0 < a1){//a0,a1,b0,b1存放修正后的两端点坐标
if(b0 < b1){
...
}else{
...
}
}else{
if(b0 < b1){
...
}else{
...//四处都是在交换对应的坐标位置来保证x坐标较小一方为起始点
}
}
测试画圆功能(显示的两个点为选中该圆后生成的控制点):
结果正常。
椭圆的绘制采用中点椭圆生成算法,本质的思想与中点圆生成类似。不同之处在于,由于椭圆的对称性与圆不完全相同,椭圆需要画出至少四分之一的圆周。
且在切线斜率为1处要进行转换,将按x轴步进改为按y轴步进(若长轴在y轴则相反),以此增加精确度。
(这里非常坑,在根据书上的步骤实现画图时,发现总是画出畸形的椭圆,比如末端变成直线什么的,断点查了一天之后发现,书上的公式印错了。。。一处少了一个平方项,一处对于P值的判断把≥和<写反了)
每一步对于下一个点的判断过程会由于分段而略有不同,将在算法中具体介绍。
下面是纠正过后的算法过程:
P1(0) = ry^2 - rx^2ry + rx^2/4(开始对区域一进行绘制)
求P1(k) = f(x + 1, y - 1/2)
若P1(k)<0,取下一个点为(x + 1, y),有递推公式:P1(k + 1) = P1(k) + 2ry^2x + 3ry2,重复步骤2直至xry2 > yrx^2。
若P1(k)≥0,取下一个点为(x + 1, y - 1),有递推公式:P1(k + 1) = P1(k) + 2ry^2x - 2rx^y + 2rx^2 + 3ry2,重复步骤2直至xry2 > yrx^2.(切线斜率经过-1)
P2(0) = ry^2(x + 1/2)^2 + rx^2(y - 1)^2 - rx^2ry^2(开始绘制区域2)
求P2(k) = f(x + 1/2, y - 1)。
若P2(k)>0,取下一个点为(x, y - 1),有递推公式:P2(k + 1) = P2(k) - 2rx^2y + 3rx^2, 重复步骤6直至x > rx。
若P2(k)≤0,取下一个点为(x + 1, y - 1),有递推公式:P2(k + 1) = P2(k) + 2ry^2x - 2rx^2y + 2ry^2 + 3rx^2, 重复步骤6直至x > rx。
实现的伪代码如下:
while(ry * ry * x < rx * rx * y){
if(p1 < 0){
p1 = p1 + 2 * ry * ry * x + 3 * ry * ry;
x++;
//求四个对称点
}else{
p1 = p1 + 2 * ry * ry * x - 2 * rx * rx * y + 3 * ry * ry + 2 * rx * rx;
x++; y--;
//求四个对称点
}
}
int p2 = ry * ry * (x + 1/2) * (x + 1/2) + rx * rx * (y - 1) * (y - 1) - rx * rx * ry * ry;
while(x <= rx && y >= 0){
if(p2 > 0){
p2 = p2 - 2 * rx * rx * y + 3 * rx * rx;
y--;
//求四个对称点
}else{
p2 = p2 + 2 * ry * ry * (x + 1) - 2 * rx * rx * (y - 1) + rx * rx;
x++; y--;
//求四个对称点
}
}
测试生成长轴在x轴与长轴在y轴的两个椭圆:
结果正常。
此外,圆和椭圆均可以通过改变控制点的位置来实现编辑操作,实现原理同直线的编辑操作。
测试编辑一个圆和一个椭圆的操作如下:
拖动右侧端点改变圆:
拖动右侧端点改变椭圆:
测试结果正常。
3.3 区域填充算法
区域填充是指从区域内的某一个象素点(种子点)开始,由内向外将填充色扩展到整个区域内的过程。
区域是指已经表示成点阵形式的填充图形,它是相互连通的一组像素的集合。
区域填充算法(边界填充算法和泛填充算法)是根据区域内的一个已知象素点(种子点)出发,找到区域内其他象素点的过程,所以把这一类算法也成为种子填充算法[3]。
本算法中实现的是边界填充算法,下面论述一下算法的可行性,并解释伪代码。
本次实现中将一个Image,或者说是我们所处理的画布视为像素4-连通,也就是说,当我们获取到一个像素的时候,视为这个像素仅与它的上下左右四个方向的四个像素是连通的(重点区分于8-连通)。我们可以通过下面这几张图来更好的理解。
本次工程设计的图形模式都较为简单,所以采用了像素4连通区域的显示模式。
首先的问题是填充算法需要访问到当前鼠标所指像素点的RGB值,原本采用过robot方法来截取屏幕获得当前画布,但后来发现robot方法的系统坐标系与我们的画布坐标系是不一样的,故此方法流产。发现到该问题后找到的解决办法是将当前画布存为一个bufferedImage对象,然后从bufferedImage中读取像素,这也成为了后来保存当前画布算法的基础[4][5]。
将画布转为bufferedImage的主要方法(paintAll()功能为将画布主体的内容都作为一个整体刷到bufImage中):
TestItem.getTi().paintAll(bufImage.getGraphics());
public Color getPixel(int x, int y){//获得当前坐标颜色信息
int rgb = bufImage.getRGB(x, y);
Color color = new Color(rgb, true);
return color;
}
算法的基本思想是采用递归,过程如下:
- 根据鼠标当前所指示的位置来获取当前这个所表示的像素值
- 将此点作为种子开始递归算法
- 选中一个点,该点是上一个选中点的4连通区域内的一个未被访问过的点
- 判断该点的颜色,若与边界颜色或填充色不同,则将该点的颜色(RGB值)设置为填充色
- 返回第三步直至没有点可以选择
不过这种算法是有一定缺陷的,本人在实验过程中就遇到了这种麻烦:栈溢出。因为递归过程是像素级的,所以很容易就会递归太多次,导致栈的大小超过可以承受的量,系统报错。
解决方法:使用栈来存储点,变递归为循环,减少系统栈空间使用量。缺点是用时可能会久一点。
修改后的算法过程如下:
- 第1步与原过程相同
- 将种子点入栈,开始循环
- 栈顶元素出栈,检测该点的RGB值,若符合条件则填充
- 查找该点的相邻4个点,若没有被入栈过且不为边界色或填充色,则将该点入栈
- 循环当栈为空时结束
具体代码如下:
public void boundaryFill4() {
pStack.push(new PointNode(paintSeedx, paintSeedy));
shape.moveTo(paintSeedx, paintSeedy);
while(!pStack.empty()){
PointNode node = pStack.pop();
pointStack[nodeNum].nodex = node.nodex; pointStack[nodeNum].nodey = node.nodey;
nodeNum++;//pointStack用来记录被访问过的点
//判断是否符合条件,不是则continue
setPixel(node.nodex, node.nodey);
PointNode[] pNode = {new PointNode(node.nodex - 1, node.nodey), new PointNode(node.nodex + 1, node.nodey), new PointNode(node.nodex, node.nodey + 1), new PointNode(node.nodex, node.nodey - 1)};
for(int i = 0; i < 4; i++){
//判断是否符合条件,不是则continue
Color thisColor = getPixel(pNode[i].nodex, pNode[i].nodey);
if((thisColor.equals(boundColor)) || (thisColor.equals(fillColor)) || (thisColor.equals(cutColor))){
continue;
}
if(inPointStack(pNode[i].nodex, pNode[i].nodey)){
continue;
}
pStack.push(pNode[i]);
}
}
}
要注意的是判断颜色时有一个大坑,就是Color类的对象无法用'=='来进行相等判断,不知道原类是怎么重载这个符号的,只能使用.equals()方法来判断颜色是否相同[6]。
测试边界填充算法(填充选择了三个封闭区域):
测试结果正常。
3.4 图形的平移、旋转、缩放算法
平移
平移算法的实现实际上体现在鼠标监听事件中。
上面已经说过,在整个系统工程运行的过程里,会有一个pressIndex来管理当前系统的状态机。若选中当前图形,则pressIndex=1。
在系统处于选中图元的状态时,会监听鼠标的press\drag\release操作,过程如下:
- 监听到press,记录当前鼠标位置,为oldPoint
- 监听到drag,记录当前鼠标位置,与oldPoint计算平移向量,对图元的控制点进行向量变换,记录为tmpSelected重构图元并重绘
- 监听到release,记录当前鼠标位置,与oldPoint计算平移向量,对图元的控制点进行向量变换,重构图元并重绘
这样保证了移动中图元也能正确地生成。也即实现了平移算法。
计算平移向量时,得到修改向量的过程如下:
Point point = new Point(e.getX(), e.getY());
int lenx = point.x - oldPoint.x;
int leny = point.y - oldPoint.y;
将lenx与leny加到图元的控制点上即可。
测试移动一个圆:
移动后:
旋转
旋转算法实现在鼠标的右键菜单中,会生成一个新的窗口来接受用户输入的旋转角度,然后根据该角度将图元进行顺时针旋转(实际上也是对图元的控制点的向量变换,生成变换矩阵并与之相乘)。
对于任意基准位置的旋转算法如下图:
在实现过程中,基准点设置为各控制点的中心,也即图元中心点。
以四边形的旋转为例,展示四边形的rotate接口:
@Override
public void rotate(double angel){
int midx = (x0 + x1) / 2;
int midy = (y0 + y1) / 2;
shape = new GeneralPath(Path2D.WIND_NON_ZERO);
for(int i = 0; i < 4; i++){
double newx = midx + (points[i].getX() - midx) * Math.cos(angel/180 * Math.PI) - (points[i].getY() - midy) * Math.sin(angel/180 * Math.PI);
double newy = midy + (points[i].getX() - midx) * Math.sin(angel/180 * Math.PI) + (points[i].getY() - midy) * Math.cos(angel/180 * Math.PI);
points[i] = new Point2D.Double(newx, newy);
}
for(int i = 0; i < 4; i++){
shape.moveTo(points[i].getX(), points[i].getY());
shape.lineTo(points[(i + 1) % 4].getX(), points[(i + 1) % 4].getY());
}
}
测试旋转一条Bezier曲线90°的几个步骤(4个蓝点为图元的控制点):
输入角度点击确定:
测试结果正常。
缩放
缩放算法也实现在鼠标的右键菜单中,用户输入缩放的倍数(倍数须大于0,大于1时为放大,小于1时为缩小)。输入后按下确定按钮会调用图元的Zoom接口,对图元的控制点进行二维变换操作。
缩放的基准位置也是图元中心,具体的变换操作如下:
@Override public void zoom(double power){
int midx = ...
int midy = ...//计算得到中点位置
x0 = (int)(midx + power * (x0 - midx));
y0 = (int)(midy + power * (y0 - midy));
...//其余点也进行同样的操作
this.constructShape();//重新构建图形的模型
}
测试缩放一条bezier曲线至原大小的一半的具体过程:
输入缩放倍数按下确定:
测试结果正常。
3.5 三次Bezier曲线生成算法
本工程实现的曲线绘制算法生成的是3次贝赛尔曲线,该曲线有4个控制点,分别为起始点、终止点和两个锚点。下面通过讲解2次贝塞尔曲线的生成来拓展至3次贝赛尔曲线[7]。
2次贝塞尔曲线的起始点为A,终止点为C,锚点为B。
- 先取AB上的一个点D,并在BC上找到点E使得AD:AB = BE:BC
- 连接DE,在DE上找到点F使得AD:AB = DF:DE
- 使点D从A移动到B,连接这过程中生成的所有F点
F点的轨迹即ABC三点生成的2次贝赛尔曲线。
下面讨论n次曲线的生成。
对于n次贝塞尔曲线来说,F点位置向量的取值有如下公式:
u的值就是2次曲线中的AD:AB的值,r为生成的曲线点的次数,i为采用的控制点。
每次要计算F点的向量值时,从r=0的控制点处启动,第一次循环得到若干个1次曲线点,第二次循环得到若干个2次曲线点,循环计算直至r=3,也即生成点在3次曲线上,包含了4个控制点作为参量。
生成过程如下:
- 将4个控制点信息导入一个数组
- 对u的取值做遍历,每次递进0.002直至为1
- 计算当前所得的F点以及下一个差值为0.001的F点的坐标
-
- r从0开始递进到3
- i从0开始递进到3 - r
- 每次循环得到新的P(i)向量为(1-u)*P(i) + uP(i+1)
- 最终得到的P(0)实际上是3次坐标下的P(0),即迭代过三次,就是要求的F点坐标
- 得到返回值,连接两个点,回到步骤2
具体的计算代码如下:
//每次递进u值,计算F点
private Point2D cubicBezier(double u, Point2D[] p) {
Point2D[] temp = new Point2D[p.length];
for (int k = 0; k < p.length; k++) temp[k] = p[k];
for (int r = 0; r < 3; r++) {
for (int i = 0; i < 4 - r - 1 ; i++) {
double x = (1 - u) * temp[i].getX() + u * temp[i + 1].getX();
double y = (1 - u) * temp[i].getY() + u * temp[i + 1].getY();
temp[i] = new Point2D.Double(x,y);
}
}
return temp[0];
}
private void drawBezier(Point2D[] p) {
for (double u = 0; u < 1; u += 0.002) {
Point2D p1= cubicBezier(u, p);
Point2D p2 = cubicBezier(u + 0.001, p);
shape.moveTo(p1.getX(), p1.getY());
shape.lineTo(p2.getX(), p2.getY());
}
}
一开始绘制的贝赛尔曲线是一条直线,由于设定两个锚点的默认值是线段的三等分点,需要拖动锚点或两个端点来改变线的形状实时重绘才能展现出曲线的特性。
下面测试生成一条贝赛尔曲线并对其端点进行调整。
曲线生成:
调整锚点:
调整端点:
测试结果正常。
3.6 裁剪算法
裁剪算法实现的是线裁剪,然后对于所有图元都调用了该裁剪的方法。
以一条直线为例,裁剪的过程如下:
- 生成裁剪边框
- 检测直线上的点与裁剪边框之间的关系
-
- 点在边框内,保留
- 点在边框外,删除该点(实际上改变了该点的颜色),重构直线的端点(对于其他图元,例如圆,重构端点会导致形状改变,故此类图元只是将该部分的点的颜色值改变),重绘
- 重复步骤2直至没有可检测点
算法伪代码如下:
private void cut(MyItem Item) {
for (选择一个Item上的点) {
if(!Point_in_cutFrame){
setPixel(Point);
//改变控制点
}
}
}
裁剪边框的生成本质上继承了四边形的生成算法,在添加时将其视为一种图元,但在显示时会单独显示,与图元分开。
为了区别于图元并便于识别,裁剪边框的颜色设置为青色,同时,在区域填充算法中也将裁剪边框的颜色加入限制条件,使得填充算法不会出现错误。
测试裁剪一个已绘制好图形的画布:
原画布:
添加裁剪边框:
测试结果正常。
特别地,裁剪边框被设定为只能出现一个,当在已有裁剪边框时再次点击裁剪按钮,原裁剪边框会消失,图形会恢复之前的状态。
原理是使用了一个单独的CutFrame变量来存储裁剪边框,对于CutFrame是否为空进行判定来判断绘制时是否进入裁剪函数。每次进行过后都会重绘。
再次按下裁剪按钮后:
测试正常。
3.7 保存当前画布
上文提到过,TestItem类中包含了一个BufferedImage来存储当前画布的所有内容,具体实现为panelImage对象。每当要存储时,会调用一个printAll函数来将TestItem创建的对象中的所有内容刷到该bufferedImage中,然后将该Image输出为result.png在工程目录下。TestItem实际上继承自JPanel,故可以调用printAll()方法。在保存成功后,控制台会有Catch!字样输出。上述的测试结果中,仅含画布的结果都是通过saveImage()方法存储的,故不再展示测试结果。实际操作时,用户只需按下保存当前画布即可完成操作。
//保存图片至本地
public void saveImage() {
JComponent tmpImage = (JComponent)ti;
tmpImage.printAll(ti.panelImage.getGraphics());
try {
ImageIO.write(ti.panelImage, "png", new File("result.png"));
System.out.println("Catch!");
} catch (IOException e) {
e.printStackTrace();
}
}
3.8 .off文件读取以及Java3D使用
首先明确加载三维模型的过程,步骤如下:
- 打开程序,准备加载文件
- 读取选择的.off文件,保存其中的点、面信息
- 逐步读取面信息,将面包含的各条边相连
- 创建Shape3D实例,加入universe中显示为Java3D图形
加载过程解析:
- 打开文件选择器
- 若选择的文件不为空,开始读文件
- 若首行为OFF,确认为.off文件,准备读取点、面、边数
- 根据读到的点数VerticeNum,循环VerticeNum次得到点的信息并存储
- 根据读到的面数FaceNum,循环FaceNum次得到面的信息并存储
主要有难度的算法在于将bufferedReader中获取的readline()字符串中的整型或浮点数提取出[8]。
算法代码如下(以提取点信息,读取浮点数为例)
double[][] verticeList = new double[verticeNum][3];
for(int i = 0; i < verticeNum; i++){
int cntv = 0;
String str = bufferedReader.readLine();
StringBuffer buffer = new StringBuffer();
char[] chars = str.toCharArray();
for(int j = 0; j < chars.length; j++){
char c = chars[j];
if(c != ' '){//浮点数被用空格分隔开,因此检测到空格就可以知道已经读取到一个浮点数
buffer.append(c);
}
if(c == ' ' || j == (chars.length - 1)){//读到空格或已经到字符串结尾,解析StringBuffer来得到浮点数
double r = Double.parseDouble(buffer.toString());
buffer = new StringBuffer();
verticeList[i][cntv] = r;
cntv++;
}
}
}
在得到所需信息后,开始使用Java3D的画线API来将三维坐标按照面的信息逐个相连。
代码如下:
for(int i = 0; i < nof; i++){
for(int j = 0; j < faceVListA[i]; j++){
LineArray lineX = new LineArray(2, LineArray.COORDINATES);
lineX.setCoordinate(0, new Point3d(verticeListA[faceListA[i][j]][0], verticeListA[faceListA[i][j]][1], verticeListA[faceListA[i][j]][2]));//[0],[1],[2]分别对应x,y,z轴坐标,需要设置线的起始点和终止点
lineX.setCoordinate(1, new Point3d(verticeListA[faceListA[i][(j + 1) % faceVListA[i]]][0], verticeListA[faceListA[i][(j + 1) % faceVListA[i]]][1], verticeListA[faceListA[i][(j + 1) % faceVListA[i]]][2]));
group.addChild(new Shape3D(lineX));//将设置好的线加入group
}
}
最后,添加光线和光源的设置,将这些一并添加进创建的universe(Java3D中的特殊对象,可以用于放置三维空间,默认x轴向右,y轴向下,z轴面对屏幕外)空间中,得以显示。
测试加载一个.off文件:
选择文件:
生成三维模型,可该窗口放大来更清楚地观察。
测试结果正常!
致谢
非常感谢助教在课程群中给出的关于3D建模的视频,其中对于Bezier曲线部分的讲解给了我很大的启发,由此才能想出曲线的正确绘制算法,真的十分感谢!
参考文献
[1] 中点画线算法-原理及实现-chengzi31-ChinaUnix博客 中点画线算法
[2] 圆形生成算法详解-CSDN博客 中点圆算法
[3] [计算机图形学经典算法] 区域填充_简述4-连通和8-连通边界填充算法,图示其填充过程-CSDN博客 区域填充算法概念介绍
[4] https://stackoverflow.com/questions/6575578/convert-a-graphics2d-to-an-image-or-bufferedimage bufferedImage的使用
[5] https://stackoverflow.com/questions/3514158/how-do-you-clone-a-bufferedimage 复制一个bufferedImage
[6] #掉过的坑#Java:Color的相等判断_java比较颜色是否相同-CSDN博客 Java Color相等判断
[7] 视频去哪了呢?_哔哩哔哩_bilibili 如何设计一个逼真的三维模型(包含Bezier曲线讲解)
[8] Java:从字符串中提取整数、浮点型数值_java截取字符串中的数字包含浮点数-CSDN博客 从字符串中提取整数、浮点数(Java)
[9] java获取图片像素点的RGB值_java pixel >> 16是什么意思-CSDN博客 获取图片像素点的RGB值
[10] Java 鼠标点击事件实例_mouseevent.button1-CSDN博客 Java鼠标点击事件
[11] Java生成exe可执行文件_java exe-CSDN博客 生成可执行文件(exe4j教程)
[12] JAVA3D安装小结(转) - 满汗全席 - 博客园 Java3D安装
[13] https://stackoverflow.com/questions/48040443/jpanel-button-is-not-at-the-correct-place?r=SearchResults Java Swing JPanel中的布局
[14] Java实现点击按钮弹出新窗体的功能实现,旧窗体不进行操作_swing对话框打开后主界面无法操作-CSDN博客 Java实现多窗口(弹出新窗体)
附中文参考文献
[1] 《计算机图形学教程》机械工业出版社
[2] 《计算机图形学:应用Java2D和3D》--HongZhang Y.Daniel Liang著 机械工业出版社(特别地,参考了书中前几章对于Java2D程序的实现模板,代码风格,以及后面的二维图形几何变换与裁剪功能的说明)