サイコロの作成プログラム

立方体の全面図

STL ファイルは立体の外側から見て、時計と反対回りに3点を指定した三角形 の集まりとして表現される。 そのため、 立方体の6個の面の座標を時計と反対まわりに列挙する。 一片1の立方体の一つの頂点を原点に置き、各面に(サイコロと同じように) 番号を振る。

(1)の面 (2)の面 (3)の面
(4)の面 (5)の面 (6)の面

そして、各面毎に外側から見て時計と反対回りに頂点が並ぶように頂点を列 挙する

プログラムは多角形 Polygon を自動的に三角形分割するようにし、 Polygon のかたまりをSTLファイルとして出力できるようにする。

プログラム

STL ファイルを出力するためのクラスライブラリの例を示します。 使用方法は test/CubeTest.java ファイルなどを読んでみてください。

なおすべてのファイルをまとめたもの はthreedprinter.zipとしてダウンロー ドできます。

stlパッケージ

stl/Vector.java

package stl;
public class Vector implements Cloneable {
	double x,y,z;
	public Vector(double _x, double _y, double _z){
		x=_x;
		y=_y;
		z=_z;
	}
	public Vector(float _x, float _y, float _z){
		x=_x;
		y=_y;
		z=_z;
	}
	public Vector(Vector point) {
		x=point.x;
		y=point.y;
		z=point.z;
	}
	public Vector() {
		x=0;
		y=0;
		z=0;
	}
	@Override
	public String toString() {
		return  x + " " + y + " " + z;
	}
	public void grow(float size){
		x *= size;
		y *= size;
		z *= size;
	}
	@Override
	public Vector clone(){
		Vector result = null;
		try {
			result  =  (Vector) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return result;
	}
	public Vector minus(Vector point) {
		Vector result = new Vector(this);
		result.x-=point.x;
		result.y-=point.y;
		result.z-=point.z;
		return result;
	}
	public Vector crossProduct(Vector v2) {
		Vector result = new Vector();
		result.x = y*v2.z-z*v2.y;
		result.y = z*v2.x - x*v2.z;
		result.z = x*v2.y - y*v2.x;
		return result;
	}
	public Vector plus(Vector point) {
		Vector result = new Vector(this);
		result.x+=point.x;
		result.y+=point.y;
		result.z+=point.z;
		return result;
	}
//	public Vector rotate(Vector normal, float d) {
//		return rotate(normal,  d);
//	}
	public Vector rotate(Vector center, Vector normal, double d) {
		Vector result = minus(center);
		/*result.x = y*v2.z-z*v2.y;
		result.y = z*v2.x - x*v2.z;
		result.z = x*v2.y - y*v2.x;
		*/
                //未実装
		return result;
	}
}

stl/Triangle.java

package stl;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
public class Triangle {
	Vector[] points;
	public Triangle(Vector p0, Vector p1, Vector p2){
		points = new Vector[3];
		points[0]=p0;
		points[1]=p1;
		points[2]=p2;
	}
	// anti clockwise
	@Override
	public String toString() {
		StringWriter sw = new StringWriter();
		PrintWriter pr = new PrintWriter(sw);
		pr.println("facet normal "+ normalVector());
		pr.println("outer loop");
		for(Vector point : points ){
			pr.println("vertex " + point);
		}
		pr.println("endloop");
		pr.println("endfacet");
		pr.close();
		return sw.toString();
	}
	private Vector normalVector() {
		Vector v1 = points[1].minus(points[0]);
		Vector v2 = points[2].minus(points[0]);
		return v1.crossProduct(v2);
	}
	public void grow(float size){
		Arrays.stream(points).forEach(x->x.grow(size));
	}
}

stl/Polygon.java

package stl;
import java.util.Arrays;
public class Polygon implements Cloneable {
	private Vector[] points;
	private Triangle[] triangles;

	public Polygon(Vector[] _points){
		points = _points;
	}
	public Polygon(Vector center, Vector initial, int n){
//		points = new Vector[n];
//		Vector r = initial.minus(center);
//		for(int i=0; i<n; i++){
//			points[i]=center.plus(r);
//			r = r.rotate(center, 2*Math.PI/n);
//		}
	}
	@Override
	public Polygon clone(){
		Polygon result = null;
		try {
			result  =  (Polygon) super.clone();
			result.points = points.clone();
			result.triangles = triangles.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return result;
	}
	public void grow(float size){
		Arrays.stream(points).forEach(x->x.grow(size));
		if(triangles != null)
			Arrays.stream(triangles).forEach(x->x.grow(size));
	}
	public void divideIntoTriangle(){
		triangles = new Triangle[points.length-2];
		for(int i=0; i<points.length-2; i++){
			triangles[i]= new Triangle(points[0],points[i+1],points[i+2]);
		}
	}
	@Override
	public String toString(){
		if(triangles==null) divideIntoTriangle();
		StringBuilder sb = new StringBuilder();
		for(Triangle t : triangles){
			sb.append(t.toString());
		}
		return sb.toString();
	}
}

stl/Matrix.java

package stl;

import java.util.Arrays;
import java.util.stream.Collectors;

public class Matrix {
	private final static int size = 3;
	private double[][] array;
	public Matrix(){
		array = new double[size][size];
	}
	public Matrix(float[][] a) {
		array = new double[size][size];
		for(int i=0; i<size; i++){
			for(int j=0; j<size; j++){
				array[i][j] = a[i][j];
			}
		}
	}

	public Matrix(double[][] a) {
		array = a.clone();
	}

	public float det(){
		float det = 0;
		float rightline ;
		float leftline ;  
		for(int i=0; i<size; i++){
			rightline = 1;
			leftline = 1;
			for(int j=0; j<size; j++){
				rightline *= array[(i+j)%size][j];
				leftline  *= array[(i+size-j)%size][j];
			}
			det = det + rightline - leftline;
		}
		return det;
	}
	public Matrix inv() {
		float det = det();
		if (det == 0) return null;
		Matrix InvA = new Matrix();
		for(int i = 0; i < size; i++) {
			int i1=(i+1)%size;
			int i2=(i+2)%size;
			for(int j = 0; j < size; j++) {
				int j1=(j+1)%size;
				int j2=(j+2)%size;
				InvA.set(i,j,(array[i1][j1]*array[i2][j2]
						-array[i1][j2]*array[i2][j1])
						/det);
			}
		}
		return InvA;
	}
	public void set(int j, int i, double f) {
		array[i][j]=f;
	}


	@Override
	public String toString() {
		return  Arrays.stream(array).map(
					x->Arrays.stream(x)
					.mapToObj(y->String.valueOf(y==-0.0?0.0:y))
					.collect(Collectors.joining(",","[","]")))
				.collect(Collectors.joining(",","[","]"))
				;
	}

}

stl/Square.java

package stl;
public class Square {
	public static Polygon toPolygon(float x0, float y0, float z0,
			float x1, float y1, float z1,
			float x2, float y2, float z2){
		Vector[] point = new Vector[4];
		point[0]=new Vector(x0,y0,z0);
		point[1]=new Vector(x1,y1,z1);
		point[2]=new Vector(x2,y2,z2);
		point[3]=new Vector(x0-x1+x2,y0-y1+y2,z0-z1+z2);
		return new Polygon(point);
	}
}

stl/Cube.java

package stl;
import java.util.Arrays;
import java.util.stream.Collectors;
public class Cube {
	private Polygon[] p;
	public Cube(float size){
		p=new Polygon[6];
		p[0]=Square.toPolygon(0, 0, 0, 0, 1, 0, 1, 1, 0);
		p[1]=Square.toPolygon(0, 0, 0, 0, 0, 1, 0, 1, 1);
		p[2]=Square.toPolygon(0, 0, 0, 1, 0, 0, 1, 0, 1);
		p[3]=Square.toPolygon(1, 1, 1, 1, 1, 0, 0, 1, 0);
		p[4]=Square.toPolygon(1, 1, 1, 1, 0, 1, 1, 0, 0);
		p[5]=Square.toPolygon(1, 1, 1, 0, 1, 1, 0, 0, 1);
		Arrays.stream(p).forEach(x->x.grow(size));
	}
	@Override
	public String toString(){
		String separator = System.getProperty("line.separator");
		return "solid cube"
				+separator
				+Arrays.stream(p)
				.map(x->x.toString())
				.collect(Collectors.joining())
				+"endsolid cube"
				+separator;
	}
}

testパッケージ(インストール不要)

test/VectorTest.java

package test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import stl.Vector;
public class VectorTest {
	private Vector p1;
	private Vector p2;
	private Vector p0;
	@Before
	public void setUp() throws Exception {
		p0 = new Vector(1,0,0);
		p1 = new Vector(0,0,1);
		p2 = new Vector(0,1,0);
		new Vector(0,0,0);
	}
	@Test
	public void testClone(){
		Vector p3 = p0.clone();
		assertEquals("1.0 0.0 0.0",p3.toString());
	}
	@Test
	public void testToString() {
		assertEquals("0.0 0.0 1.0",p1.toString());
	}

	@Test
	public void testMinus() {
		Vector p3 = p1.minus(p2);
		assertEquals("0.0 -1.0 1.0",p3.toString());
	}
	@Test
	public void testPlus() {
		Vector p3 = p1.plus(p2);
		assertEquals("0.0 1.0 1.0",p3.toString());
	}
	@Test
	public void testCrossProduct() {
		Vector p4 = p1.crossProduct(p2);
		assertEquals("-1.0 0.0 0.0",p4.toString());
	}
	@Test
	public void testRotate(){
//		Vector p3 = p0.rotate(p1, Math.PI/2.0);
//		assertEquals("0.0 1.0 0.0",p3.toString());
	}

}

test/TriangleTest.java

package test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import stl.Vector;
import stl.Triangle;
public class TriangleTest {
	private Triangle t0;
	@Before
	public void setUp() throws Exception {
		Vector p0 = new Vector(0,0,0);
		Vector p1 = new Vector(1,0,0);
		Vector p2 = new Vector(0,1,0);
		t0 = new Triangle(p0,p1,p2);
	}
	@Test
	public void testToString() {
		String separator = System.getProperty("line.separator");
		String expect = "facet normal 0.0 0.0 1.0"+separator
				+"outer loop"+separator
				+"vertex 0.0 0.0 0.0"+separator
				+"vertex 1.0 0.0 0.0"+separator
				+"vertex 0.0 1.0 0.0"+separator
				+"endloop"+separator
				+"endfacet"+separator;
		assertEquals(expect,t0.toString());
	}
}

test/PolygonTest.java

package test;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import stl.Vector;
import stl.Polygon;
public class PolygonTest {
	private Polygon p0;
	private final String NL= System.getProperty("line.separator");
	@Before
	public void setUp() throws Exception {
		Vector[] p = {new Vector(0,0,0),
				new Vector(1,0,0),
				new Vector(1,1,0),
				new Vector(0,1,0)};
		p0 = new Polygon(p);
	}
	@Test
	public void testToString() {
		String expect = "facet 0.0 0.0 1.0"+NL
				+"outer loop"+NL
				+"vertex 0.0 0.0 0.0"+NL
				+"vertex 1.0 0.0 0.0"+NL
				+"vertex 1.0 1.0 0.0"+NL
				+"endloop"+NL
				+"endfacet"+NL
				+"facet 0.0 0.0 1.0"+NL
				+"outer loop"+NL
				+"vertex 0.0 0.0 0.0"+NL
				+"vertex 1.0 1.0 0.0"+NL
				+"vertex 0.0 1.0 0.0"+NL
				+"endloop"+NL
				+"endfacet"+NL;
		assertEquals(expect,p0.toString());
	}
}

test/MatrixTest.java

package test;
import static org.junit.Assert.*;
import stl.Matrix;
import org.junit.Before;
import org.junit.Test;
public class MatrixTest {
	private static final float e=(float) 1E-5;
	private Matrix a;
	private Matrix i;
	private Matrix rz;
	private Matrix irz;
	private Matrix rx;
	private Matrix irx;
	private Matrix ry;
	private Matrix iry;
	@Before
	public void setUp() throws Exception {
		a = new Matrix(new float[][]{{1,2,3},{4,5,6},{7,8,9}});
		i = new Matrix(new float[][]{{1,0,0},{0,1,0},{0,0,1}});
		 double c = Math.cos(Math.PI/2);
		 double s = Math.sin(Math.PI/2);
		rz= new Matrix(new double[][]{{c*2,-s*2,0},{s*2,c*2,0},{0,0,2}});
		irz= new Matrix(new double[][]{{c/2,s/2,0},{-s/2,c/2,0},{0,0,0.5}});
		rx= new Matrix(new double[][]{{2,0,0},{0,c*2,-s*2},{0,s*2,c*2}});
		irx= new Matrix(new double[][]{{0.5,0,0},{0,c/2,s/2},{0,-s/2,c/2}});
		ry= new Matrix(new double[][]{{c*2,0,-s*2,0},{0,2,0},{s*2,0,c*2}});
		iry= new Matrix(new double[][]{{c/2,0,s/2},{0,0.5,0},{-s/2,0,c/2}});
	}
	@Test
	public void testDet() {
		assertEquals(1,i.det(),e);
		assertEquals(8,rx.det(),e);
		assertEquals(0.125,irx.det(),e);
		assertEquals(8,ry.det(),e);
		assertEquals(0.125,iry.det(),e);
		assertEquals(8,rz.det(),e);
		assertEquals(0.125,irz.det(),e);
		assertEquals(0,a.det(),e);
	}
	@Test
	public void testInv() {
		assertEquals(i.toString(),i.inv().toString());
		assertEquals(irx.toString(),rx.inv().toString());
		assertEquals(iry.toString(),ry.inv().toString());
		assertEquals(irz.toString(),rz.inv().toString());
		assertEquals(null,a.inv());
	}
}

test/CubeTest.java

package test;
import static org.junit.Assert.*;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import stl.Cube;
public class CubeTest {
	private Cube cube;
	@Before
	public void setUp() throws Exception {
		cube = new Cube(2);
	}
	@Test
	public void test() throws FileNotFoundException {
		String separator = System.getProperty("line.separator");
		FileReader f = new FileReader("expectcube.txt");
		BufferedReader br = new BufferedReader(f);
		String expect = br.lines().collect(Collectors.joining(separator))
				+separator;
		assertEquals(expect,cube.toString());
	}

}

プロジェクトに直接加えるファイル

expectcube.txt... test.CubeTest で使用 するファイル

付録

軸を指定した回転のアルゴリズム

以下は、実装はしていませんが、有益なアルゴリズムなので紹介します。

p を 中心点 c を中心に軸ベクトル n の回りに t だけ回転させたい

  1. 中心点が原点に来るように平行移動。このための平行移動ベクトルは -c
  2. 軸ベクトルが z 軸になるような回転行列を求める。 この回転行列を N とする。
  3. t だけ回転する回転行列を T とする。
  4. 求める座標は N-1 T N p - c + c で 求まる。
T = cost -sint 0 sint cost 0 0 0 1

軸ベクトルを z 軸を中心にx-z 平面上に回転させる

軸ベクトルと z 軸で作る平面の法線ベクトルは軸ベクトルと 0 0 1 との外積で 求まる。

nx ny nz × 0 0 1 = ny -nx 0

これは x,y 平面に乗る これをz 軸回りに回転させてy 軸に一致させると、 n ベクトルは x-z 平面上に乗る。

c1 -s1 0 s1 c1 0 0 0 1 ny -nx 0 = ny c1 + nx s1 ny s1 - nx c1 0
ny c1 + nx s1 = 0 c12 + s12 = 1

この c1 , s1 による行列を N1 とし、 n 1 = N1 n と置く。 n 1 は x-z 平面上に乗っているので、 y 座標は 0 であることに注意。

x-z平面上の軸ベクトルを y 軸を中心に z 軸に回転させる

n1 = n1,x 0 n1,z は x-z 上のベクトルなので、 y 軸の回りに回転させて z 軸に一致させる

c2 0 -s2 0 1 0 s2 0 c2 n1,x 0 n1,z = n1,x c2 - n1,z s2 0 n1,x s2 + n1,z c2
n1,x c2 - n1,z s2 = 0 c22 + s22 = 1

以上より、次が求まる

N = N2 N1 = c2 0 -s2 0 1 0 s2 0 c2 c1 -s1 0 s1 c1 0 0 0 1

アフィン変換の活用

変換後の座標を p ' としたとき、 変換は次で得られる。

p ' = N-1 T N p - c + c

c ' = I - N-1 T N c と置くと次のように書き直せる。

p ' = N-1 T N p + c '

ここで、次元を1つ上げたアフィン変換を次のように定義する。

p ' 1 = N-1 T N c ' 0   0   0 1 p 1

参考文献

  1. おしえてgoo 3次元ベクトル をある軸ベクトルで回転させたい