├── .gitignore
├── project.properties
├── res
├── drawable-hdpi
│ └── icon.png
├── drawable-ldpi
│ └── icon.png
├── drawable-mdpi
│ └── icon.png
├── layout
│ ├── main.xml
│ ├── picker.xml
│ └── picker_item.xml
└── values
│ └── strings.xml
├── src
├── Useful.java
├── GLUseful.java
├── PolyAccelerationDecelerationInterpolator.java
├── BundledSavedState.java
├── Vec3f.java
├── Rotation.java
├── Picker.java
├── GeomBuilder.java
├── Main.java
├── ObjectView.java
└── ObjReader.java
├── AndroidManifest.xml
├── INSTALL
├── README
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /gen/
3 | /local.properties
4 | /keystore.properties
5 |
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # Target API to build project against
2 | target=android-7
3 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ldo/ObjViewer_Android/HEAD/res/drawable-hdpi/icon.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ldo/ObjViewer_Android/HEAD/res/drawable-ldpi/icon.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ldo/ObjViewer_Android/HEAD/res/drawable-mdpi/icon.png
--------------------------------------------------------------------------------
/res/layout/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/res/layout/picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
18 |
24 |
25 |
--------------------------------------------------------------------------------
/res/layout/picker_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
15 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Useful.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.android.useful;
2 | /*
3 | Some basic useful stuff.
4 |
5 | Copyright 2013 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | public class Useful
21 | {
22 |
23 | public static double GetTime()
24 | /* returns current UTC time in seconds. */
25 | {
26 | return
27 | System.currentTimeMillis() / 1000.0;
28 | } /*GetTime*/
29 |
30 | } /*Useful*/;
31 |
--------------------------------------------------------------------------------
/src/GLUseful.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.GLUseful;
2 | /*
3 | Useful OpenGL-ES-1.1-related definitions.
4 |
5 | Copyright 2013 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | public class GLUseful
21 | {
22 | public static final android.opengl.GLES11 gl = new android.opengl.GLES11(); /* for easier references */
23 | public static final java.util.Locale StdLocale = java.util.Locale.US; /* for when I don't actually want a locale */
24 | } /*GLUseful*/;
25 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ObjViewer
4 |
5 |
6 | Open...
7 | Reset View
8 | Random View
9 | Lighting...
10 | Orient Faces...
11 |
12 |
13 | Select .obj File
14 | Lighting
15 | Front Face Orientation
16 |
17 |
18 | Open
19 | On
20 | Off
21 | Clockwise
22 | Anticlockwise
23 |
24 |
25 | Use Menu/Open... to open a .obj file
26 | Error loading file: %s
27 |
28 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/PolyAccelerationDecelerationInterpolator.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.ObjViewer;
2 | /*
3 | Example animation interpolator that implements polynomial acceleration
4 | and deceleration.
5 |
6 | Copyright 2011 by Lawrence D'Oliveiro .
7 |
8 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
9 | use this file except in compliance with the License. You may obtain a copy of
10 | the License at
11 |
12 | http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 | License for the specific language governing permissions and limitations under
18 | the License.
19 | */
20 |
21 | public class PolyAccelerationDecelerationInterpolator
22 | implements android.view.animation.Interpolator
23 | {
24 | public final float Power;
25 | /* the higher the power, the more relative time is spent accelerating
26 | and decelerating, and so the faster the maximum speed is */
27 |
28 | public PolyAccelerationDecelerationInterpolator
29 | (
30 | float Power
31 | )
32 | {
33 | this.Power = Power;
34 | } /*PolyAccelerationDecelerationInterpolator*/
35 |
36 | public float getInterpolation
37 | (
38 | float RelTime
39 | )
40 | {
41 | final float Offset = RelTime > 0.5f ? 1.0f - RelTime : RelTime;
42 | final float Y = (float)Math.pow(2.0f * Offset, Power + 1.0f) / 2.0f;
43 | return
44 | RelTime > 0.5f ? 1.0f - Y : Y;
45 | } /*getInterpolation*/
46 |
47 | } /*PolyAccelerationDecelerationInterpolator*/
48 |
--------------------------------------------------------------------------------
/INSTALL:
--------------------------------------------------------------------------------
1 | To build the ObjViewer app for Android, you will need the Android SDK,
2 | available from , along with its
3 | prerequisites, including a Java developer kit (either OpenJDK or the
4 | Sun JDK) and Ant.
5 |
6 | You will need to add a couple of files to the top of the source
7 | tree: local.properties, and keystore.properties. These are excluded
8 | from the Git repository because their contents are specific to your
9 | setup.
10 |
11 | local.properties defines a property sdk.dir, which points to the
12 | top-level directory where you installed the Android SDK. E.g.
13 |
14 | sdk.dir=/path/to/android-sdk-linux_x86
15 |
16 | keystore.properties defines three properties to do with signing of
17 | the application package: keystore.path is the path to your keystore
18 | file, keystore.alias is the name (alias) of the key in the keystore
19 | to use, and keystore.password is the password for accessing that key.
20 | E.g.
21 |
22 | keystore.path=/home/ldo/my.keystore
23 | keystore.alias=my-key
24 | keystore.password=please
25 |
26 | Because it has to contain a password, I would recommend you do NOT make
27 | this file readable by anybody but its owner.
28 |
29 | If you don't already have a keystore, you can create one with
30 | the JDK keytool command, something like this:
31 |
32 | keytool -genkeypair -keystore keystorename -storepass keystorepassword \
33 | -keyalg RSA -validity $((25 * 365)) -alias keyalias -keysize 2048 \
34 | -dname "CN=J Random Hacker, O=HackerCo, L=Anytown, ST=Anystate, C=US"
35 |
36 | substituting your own filename for keystorename, your own password
37 | for keystorepassword, your own name for the key for keyalias, and of course
38 | your own identification details in the value for -dname.
39 |
40 | Now you should be able to build the complete package with the
41 | following command:
42 |
43 | ant signed
44 |
45 | If this completes without errors, you should have a file
46 | bin/ObjViewer-release.apk, which is the application package.
47 |
48 | You can also install this automatically to an Android device
49 | connected via USB. First, make sure "USB Debugging" is enabled
50 | on that device. Now do
51 |
52 | ant install
53 |
54 | which will invoke the building and signing, and then install the
55 | package on the default connected Android device.
56 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | ObjViewer is a sample Android application illustrating various aspects
2 | of using OpenGL-ES 3D graphics:
3 | * GeomBuilder: basic management of 3D object geometry and rendering
4 | * ObjReader: simple reading of common .obj-format model files and
5 | associated .mtl material-definition files
6 | * Rotation: generation of 3D rotation transformations using unit
7 | quaternions
8 | * ObjectView: basic on-screen display of a 3D model with simple
9 | lighting, allowing interactive rotation to any orientation, and
10 | also demonstrating animated rotations
11 |
12 | Copy the .obj file that you want to view (and associated .mtl file, if
13 | any) onto a directory named “Models” on the SD card of the Android
14 | device. It should then appear in the list that is shown when you
15 | select “Open...” from the options menu in ObjViewer.
16 |
17 | You can interactively adjust the orientation of the object by dragging
18 | across the view; drag across the middle part to rotate about the X
19 | (horizontal) and Y (vertical) axes, and drag around the periphery to
20 | rotate about the Z axis (perpendicular to the view) as well.
21 |
22 | To see any colours that might be defined for the surfaces of your model,
23 | turn on lighting.
24 |
25 | If parts of the surface of the object seem to disappear, try switching
26 | the order of vertices in a forward-looking face under “Orient Faces...”.
27 |
28 | “Reset View” animates rotating the object back to its initial orientation.
29 | “Random View” animates rotating the object to a random orientation.
30 |
31 | If you want to find downloadable models to view, here are a couple of
32 | lists of likely websites:
33 |
34 | .
35 |
36 | If you want to create your own models, Blender
37 | is an amazing package, well worth at least getting a basic understanding
38 | of how to create simple models and scenes with it. But don’t try to learn it
39 | on your own; you’ll find plenty of tutorials to get you started--just
40 | follow the links from the Blender site.
41 |
42 | Contents:
43 | src/ -- Java sources for the Android app
44 | res/ -- resources for the Android app
45 | AndroidManifest.xml, build.xml, *.properties -- for driving
46 | Google's Android build tools (note that you will have to
47 | provide a couple more of these--see INSTALL for details)
48 | README -- this file
49 | INSTALL -- build/installation instructions
50 | LICENSE -- licence (Apache 2.0, same as Google’s sample code)
51 |
52 | Like the icon? It's by Tehkseven--more here
53 | .
54 |
55 | Lawrence D'Oliveiro
56 |
--------------------------------------------------------------------------------
/src/BundledSavedState.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.android.useful;
2 | /*
3 | Generic class for easier implementation of save/restore instance state
4 | in a custom View subclass. This class holds all the state as items in
5 | a Bundle.
6 |
7 | Copyright 2012 by Lawrence D'Oliveiro .
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy of
11 | the License at
12 |
13 | http://www.apache.org/licenses/LICENSE-2.0
14 |
15 | Unless required by applicable law or agreed to in writing, software
16 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
18 | License for the specific language governing permissions and limitations under
19 | the License.
20 | */
21 |
22 | import android.os.Parcelable;
23 | import android.os.Parcel;
24 | import android.os.Bundle;
25 | import android.view.AbsSavedState;
26 |
27 | public class BundledSavedState extends AbsSavedState
28 | /* Create and return one of these in the onSaveInstanceState method in your custom View.
29 | Sample call sequence:
30 | final android.os.Bundle MyState = new android.os.Bundle();
31 | MyState.putxxx("MyName", MyValue);
32 | ... insert any other custom fields into the Bundle ...
33 | return
34 | new BundledSavedState(super.onSaveInstanceState(), MyState);
35 |
36 | Then, your onRestoreInstanceState method can cast its argument to an
37 | instance of this class, and process the saved fields appropriately, e.g.:
38 | super.onRestoreInstanceState(((BundledSavedState)SavedState).SuperState);
39 | final android.os.Bundle MyState = ((BundledSavedState)SavedState).MyState;
40 | MyValue = MyState.getxxx("MyName", MyDefault);
41 | ... retrieve any other custom fields ...
42 | */
43 | {
44 |
45 | public static final Parcelable.Creator CREATOR =
46 | new Parcelable.Creator()
47 | {
48 | public BundledSavedState createFromParcel
49 | (
50 | Parcel SavedState
51 | )
52 | {
53 | final AbsSavedState SuperState =
54 | AbsSavedState.CREATOR.createFromParcel(SavedState);
55 | final Bundle MyState = SavedState.readBundle();
56 | return
57 | new BundledSavedState(SuperState, MyState);
58 | } /*createFromParcel*/
59 |
60 | public BundledSavedState[] newArray
61 | (
62 | int NrElts
63 | )
64 | {
65 | return
66 | new BundledSavedState[NrElts];
67 | } /*newArray*/
68 | } /*Parcelable.Creator*/;
69 |
70 | public final Parcelable SuperState; /* pass to super.onRestoreInstanceState() */
71 | public final Bundle MyState; /* all your custom state information saved here */
72 |
73 | public BundledSavedState
74 | (
75 | Parcelable SuperState, /* result from super.onSaveInstanceState() */
76 | Bundle MyState /* put all your custom state information here */
77 | )
78 | {
79 | super(SuperState);
80 | this.SuperState = SuperState;
81 | this.MyState = MyState;
82 | } /*BundledSavedState*/
83 |
84 | public void writeToParcel
85 | (
86 | Parcel SavedState,
87 | int Flags
88 | )
89 | {
90 | super.writeToParcel(SavedState, Flags);
91 | SavedState.writeBundle(MyState);
92 | } /*writeToParcel*/
93 |
94 | } /*BundledSavedState*/
95 |
--------------------------------------------------------------------------------
/src/Vec3f.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.GLUseful;
2 | /*
3 | functional 3D vector operations
4 |
5 | Copyright 2011 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | public class Vec3f
21 | /* 3D vectors */
22 | {
23 | public final float x, y, z, w;
24 |
25 | public Vec3f
26 | (
27 | float x,
28 | float y,
29 | float z
30 | )
31 | {
32 | this.x = x;
33 | this.y = y;
34 | this.z = z;
35 | this.w = 1.0f;
36 | } /*Vec3f*/
37 |
38 | public Vec3f
39 | (
40 | float x,
41 | float y,
42 | float z,
43 | float w
44 | )
45 | {
46 | this.x = x;
47 | this.y = y;
48 | this.z = z;
49 | this.w = w;
50 | } /*Vec3f*/
51 |
52 | public Vec3f
53 | (
54 | float[] v
55 | )
56 | {
57 | if (v.length != 3 && v.length != 4)
58 | {
59 | throw new RuntimeException("need 3 or 4 floats to make a vector");
60 | } /*if*/
61 | x = v[0];
62 | y = v[1];
63 | z = v[2];
64 | w = v.length == 4 ? v[3] : 1.0f;
65 | } /*Vec3f*/
66 |
67 | public float[] to_floats
68 | (
69 | int nrelts /* 3 or 4 */
70 | )
71 | {
72 | float[] v;
73 | switch (nrelts)
74 | {
75 | case 3:
76 | v = new float[] {x, y, z};
77 | break;
78 | case 4:
79 | v = new float[] {x, y, z, w};
80 | break;
81 | default:
82 | throw new RuntimeException("vector can only convert to 3 or 4 floats");
83 | /* break; */
84 | } /*switch*/
85 | return
86 | v;
87 | } /*to_floats*/
88 |
89 | public static Vec3f zero()
90 | {
91 | return
92 | new Vec3f(0.0f, 0.0f, 0.0f);
93 | } /*zero*/
94 |
95 | public Vec3f neg()
96 | {
97 | return
98 | new Vec3f(-x, -y, -z, w);
99 | } /*neg*/
100 |
101 | public Vec3f add
102 | (
103 | Vec3f v
104 | )
105 | {
106 | return
107 | new Vec3f(x + v.x, y + v.y, z + v.z);
108 | } /*add*/
109 |
110 | public Vec3f sub
111 | (
112 | Vec3f v
113 | )
114 | {
115 | return
116 | new Vec3f(x - v.x, y - v.y, z - v.z);
117 | } /*sub*/
118 |
119 | public Vec3f mul
120 | (
121 | float s
122 | )
123 | {
124 | return
125 | new Vec3f(x * s, y * s, z * s, w);
126 | } /*mul*/
127 |
128 | public Vec3f recip()
129 | {
130 | return
131 | new Vec3f(1.0f / x, 1.0f / y, 1.0f / z);
132 | } /*recip*/
133 |
134 | public float dot
135 | (
136 | Vec3f v
137 | )
138 | {
139 | return
140 | v.x * this.x + v.y * this.y + v.z * this.z;
141 | } /*dot*/
142 |
143 | public Vec3f cross
144 | (
145 | Vec3f v
146 | )
147 | {
148 | return
149 | new Vec3f
150 | (
151 | this.y * v.z - v.y * this.z,
152 | this.z * v.x - v.z * this.x,
153 | this.x * v.y - v.x * this.y
154 | );
155 | } /*cross*/
156 |
157 | public float azimuth()
158 | /* returns the angle between the x-axis and the line from the origin to the point. */
159 | {
160 | return
161 | (float)Math.atan2(y, x);
162 | } /*azimuth*/
163 |
164 | public float elevation()
165 | /* returns the angle between the x-y plane and the line from the origin to the point. */
166 | {
167 | return
168 | (float)Math.atan2(z, (float)Math.sqrt(x * x + y * y));
169 | } /*elevation*/
170 |
171 | public float abs()
172 | /* returns the distance between the point and the origin. */
173 | {
174 | return
175 | (float)Math.sqrt((x * x + y * y + z * z) / (w * w));
176 | } /*abs*/
177 |
178 | public Vec3f unit()
179 | {
180 | final float abs = this.abs();
181 | return
182 | new Vec3f(x / abs, y / abs, z / abs);
183 | } /*unit*/
184 |
185 | public Vec3f norm()
186 | /* rescales so w = 1. */
187 | {
188 | return
189 | new Vec3f(x / w, y / w, z / w);
190 | } /*norm*/
191 |
192 | } /*Vec3f*/
193 |
--------------------------------------------------------------------------------
/src/Rotation.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.GLUseful;
2 | /*
3 | Quaternion representation of 3D rotation transformations.
4 |
5 | Copyright 2011, 2013 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | import static nz.gen.geek_central.GLUseful.GLUseful.gl;
21 |
22 | public class Rotation implements android.os.Parcelable
23 | {
24 | public final float c, s; /* cosine and sine of half the rotation angle */
25 | public final float x, y, z; /* rotation axis unit vector */
26 | /* actually quaternion is (c, s * x, s * y, s * z) */
27 |
28 | public Rotation
29 | (
30 | float Angle,
31 | boolean Degrees, /* false for radians */
32 | float X,
33 | float Y,
34 | float Z
35 | )
36 | /* constructs a Rotation that rotates by the specified angle
37 | about the axis direction (X, Y, Z). */
38 | {
39 | final double Theta = Float.isNaN(Angle) ? 0.0 : (Degrees ? Math.toRadians(Angle) : Angle) / 2;
40 | c = (float)Math.cos(Theta);
41 | s = (float)Math.sin(Theta);
42 | final float Mag = (float)Math.sqrt(X * X + Y * Y + Z * Z);
43 | x = X / Mag;
44 | y = Y / Mag;
45 | z = Z / Mag;
46 | } /*Rotation*/
47 |
48 | public Rotation
49 | (
50 | float Angle,
51 | boolean Degrees, /* false for radians */
52 | Vec3f Axis
53 | )
54 | {
55 | this(Angle, Degrees, Axis.x, Axis.y, Axis.z);
56 | } /*Rotation*/
57 |
58 | public static final Rotation Null = new Rotation(0, 0, 0, 1);
59 | /* represents no rotation at all */
60 |
61 | private Rotation
62 | (
63 | float c,
64 | float x,
65 | float y,
66 | float z
67 | )
68 | /* internal-use constructor with directly-computed components. Note
69 | this does not compensate for accumulated rounding errors. */
70 | {
71 | this.c = c;
72 | this.s = (float)Math.sqrt(x * x + y * y + z * z);
73 | this.x = x / this.s;
74 | this.y = y / this.s;
75 | this.z = z / this.s;
76 | } /*Rotation*/
77 |
78 | private Rotation
79 | (
80 | float c,
81 | float s,
82 | float x,
83 | float y,
84 | float z
85 | )
86 | /* internal-use constructor with directly-computed components. Note
87 | this does not compensate for accumulated rounding errors. */
88 | {
89 | this.c = c;
90 | this.s = s;
91 | this.x = x;
92 | this.y = y;
93 | this.z = z;
94 | } /*Rotation*/
95 |
96 | public static final android.os.Parcelable.Creator CREATOR =
97 | /* restore state from a Parcel. */
98 | new android.os.Parcelable.Creator()
99 | {
100 | public Rotation createFromParcel
101 | (
102 | android.os.Parcel Post
103 | )
104 | {
105 | final android.os.Bundle MyState = Post.readBundle();
106 | return
107 | new Rotation
108 | (
109 | MyState.getFloat("c", Null.c),
110 | MyState.getFloat("s", Null.s),
111 | MyState.getFloat("x", Null.x),
112 | MyState.getFloat("y", Null.y),
113 | MyState.getFloat("z", Null.z)
114 | );
115 | } /*createFromParcel*/
116 |
117 | public Rotation[] newArray
118 | (
119 | int NrElts
120 | )
121 | {
122 | return
123 | new Rotation[NrElts];
124 | } /*newArray*/
125 | } /*Parcelable.Creator*/;
126 |
127 | @Override
128 | public int describeContents()
129 | {
130 | return
131 | 0; /* nothing special */
132 | } /*describeContents*/
133 |
134 | @Override
135 | public void writeToParcel
136 | (
137 | android.os.Parcel Post,
138 | int Flags
139 | )
140 | /* save state to a Parcel. */
141 | {
142 | final android.os.Bundle MyState = new android.os.Bundle();
143 | MyState.putFloat("c", c);
144 | MyState.putFloat("s", s);
145 | MyState.putFloat("x", x);
146 | MyState.putFloat("y", y);
147 | MyState.putFloat("z", z);
148 | Post.writeBundle(MyState);
149 | } /*writeToParcel*/
150 |
151 | public Rotation inv()
152 | /* returns rotation by the opposite angle around the same axis. Or alternatively,
153 | the same angle around the opposite-pointing axis . */
154 | {
155 | return
156 | new Rotation(c, -s, x, y, z);
157 | } /*inv*/
158 |
159 | public Rotation mul
160 | (
161 | Rotation that
162 | )
163 | /* returns composition with another rotation. */
164 | {
165 | final float s2 = this.s * that.s;
166 | return
167 | new Rotation
168 | (
169 | this.c * that.c - (this.x * that.x + this.y * that.y + this.z * that.z) * s2,
170 | (this.y * that.z - this.z * that.y) * s2 + this.c * that.x * that.s + that.c * this.x * this.s,
171 | (this.z * that.x - this.x * that.z) * s2 + this.c * that.y * that.s + that.c * this.y * this.s,
172 | (this.x * that.y - this.y * that.x) * s2 + this.c * that.z * that.s + that.c * this.z * this.s
173 | );
174 | } /*mul*/
175 |
176 | public Rotation div
177 | (
178 | Rotation that
179 | )
180 | /* returns the difference from another rotation. */
181 | {
182 | return
183 | mul(that.inv());
184 | } /*div*/
185 |
186 | public Rotation mul
187 | (
188 | float Frac /* can also be negative or > 1 */
189 | )
190 | /* returns the specified fraction of the rotation. */
191 | {
192 | return
193 | new Rotation
194 | (
195 | GetAngle(false) * Frac, false, x, y, z
196 | );
197 | } /*mul*/
198 |
199 | public float GetAngle
200 | (
201 | boolean Degrees /* false for radians */
202 | )
203 | /* returns the rotation angle. */
204 | {
205 | final double Theta = Math.atan2(s, c);
206 | return
207 | 2 * (float)(Degrees ? Math.toDegrees(Theta) : Theta);
208 | } /*GetAngle*/
209 |
210 | public Vec3f GetAxis()
211 | {
212 | return
213 | new Vec3f(x, y, z);
214 | } /*GetAxis*/
215 |
216 | public void Apply()
217 | /* applies the rotation to the currently-selected GL matrix. */
218 | {
219 | gl.glRotatef(GetAngle(true), x, y, z);
220 | } /*Apply*/
221 |
222 | public String toString()
223 | {
224 | return
225 | String.format
226 | (
227 | GLUseful.StdLocale,
228 | "Rotation(%e, %e, %e, %e, %e)",
229 | c, s, x, y, z
230 | );
231 | } /*toString*/
232 |
233 | } /*Rotation*/;
234 |
--------------------------------------------------------------------------------
/src/Picker.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.ObjViewer; /* must be in same package as app in order to find resources */
2 | /*
3 | let the user choose a file to load
4 |
5 | Copyright 2011-2014 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | import android.view.View;
21 |
22 | public class Picker extends android.app.Activity
23 | {
24 | /* required extra information passed in launch intent: */
25 | public static final String LookInID = "nz.gen.geek_central.android.useful.Picker.LookIn";
26 | /* array of strings representing names of directories in which to look for files */
27 | /* ones not beginning with “/” are interpreted as subdirectories in external storage */
28 | public static final String ExtensionID = "nz.gen.geek_central.android.useful.Picker.Extension";
29 | /* extension of filenames to show */
30 |
31 | android.widget.ListView PickerListView;
32 | SelectedItemAdapter PickerList;
33 |
34 | static class PickerItem
35 | {
36 | String FullPath;
37 | boolean Selected;
38 |
39 | public PickerItem
40 | (
41 | String FullPath
42 | )
43 | {
44 | this.FullPath = FullPath;
45 | this.Selected = false;
46 | } /*PickerItem*/
47 |
48 | public String toString()
49 | /* returns the display name for the item. I use
50 | the unqualified filename. */
51 | {
52 | return
53 | new java.io.File(FullPath).getName();
54 | } /*toString*/
55 |
56 | } /*PickerItem*/
57 |
58 | class SelectedItemAdapter extends android.widget.ArrayAdapter
59 | {
60 | final int ResID;
61 | final android.view.LayoutInflater TemplateInflater;
62 | PickerItem CurSelected;
63 | android.widget.RadioButton LastChecked;
64 |
65 | class OnSetCheck implements View.OnClickListener
66 | {
67 | final PickerItem MyItem;
68 |
69 | public OnSetCheck
70 | (
71 | PickerItem TheItem
72 | )
73 | {
74 | MyItem = TheItem;
75 | } /*OnSetCheck*/
76 |
77 | public void onClick
78 | (
79 | View TheView
80 | )
81 | {
82 | if (MyItem != CurSelected)
83 | {
84 | /* only allow one item to be selected at a time */
85 | if (CurSelected != null)
86 | {
87 | CurSelected.Selected = false;
88 | LastChecked.setChecked(false);
89 | } /*if*/
90 | LastChecked =
91 | TheView instanceof android.widget.RadioButton ?
92 | (android.widget.RadioButton)TheView
93 | :
94 | (android.widget.RadioButton)
95 | ((android.view.ViewGroup)TheView).findViewById(R.id.file_item_checked);
96 | CurSelected = MyItem;
97 | MyItem.Selected = true;
98 | LastChecked.setChecked(true);
99 | } /*if*/
100 | } /*onClick*/
101 | } /*OnSetCheck*/
102 |
103 | SelectedItemAdapter
104 | (
105 | android.content.Context TheContext,
106 | int ResID,
107 | android.view.LayoutInflater TemplateInflater
108 | )
109 | {
110 | super(TheContext, ResID);
111 | this.ResID = ResID;
112 | this.TemplateInflater = TemplateInflater;
113 | CurSelected = null;
114 | LastChecked = null;
115 | } /*SelectedItemAdapter*/
116 |
117 | @Override
118 | public View getView
119 | (
120 | int Position,
121 | View ReuseView,
122 | android.view.ViewGroup Parent
123 | )
124 | {
125 | View TheView = ReuseView;
126 | if (TheView == null)
127 | {
128 | TheView = TemplateInflater.inflate(ResID, null);
129 | } /*if*/
130 | final PickerItem ThisItem = this.getItem(Position);
131 | ((android.widget.TextView)TheView.findViewById(R.id.select_file_name))
132 | .setText(ThisItem.toString());
133 | final android.widget.RadioButton ThisChecked =
134 | (android.widget.RadioButton)TheView.findViewById(R.id.file_item_checked);
135 | ThisChecked.setChecked(ThisItem.Selected);
136 | final OnSetCheck ThisSetCheck = new OnSetCheck(ThisItem);
137 | ThisChecked.setOnClickListener(ThisSetCheck);
138 | /* otherwise radio button can get checked but I don't notice */
139 | TheView.setOnClickListener(ThisSetCheck);
140 | return
141 | TheView;
142 | } /*getView*/
143 |
144 | } /*SelectedItemAdapter*/
145 |
146 | @Override
147 | public void onCreate
148 | (
149 | android.os.Bundle ToRestore
150 | )
151 | {
152 | super.onCreate(ToRestore);
153 | setContentView(R.layout.picker);
154 | PickerList = new SelectedItemAdapter(this, R.layout.picker_item, getLayoutInflater());
155 | PickerListView = (android.widget.ListView)findViewById(R.id.item_list);
156 | PickerListView.setAdapter(PickerList);
157 | PickerList.setNotifyOnChange(false);
158 | PickerList.clear();
159 | {
160 | final String ExternalStorage =
161 | android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
162 | final String Extension = getIntent().getStringExtra(ExtensionID);
163 | for (String Here : getIntent().getStringArrayExtra(LookInID))
164 | {
165 | final java.io.File ThisDir = new java.io.File
166 | (
167 | (Here.startsWith("/") ?
168 | ""
169 | :
170 | ExternalStorage + "/"
171 | )
172 | +
173 | Here
174 | );
175 | if (ThisDir.isDirectory())
176 | {
177 | for (java.io.File Item : ThisDir.listFiles())
178 | {
179 | if (Item.getName().endsWith(Extension))
180 | {
181 | PickerList.add(new PickerItem(Item.getAbsolutePath()));
182 | } /*if*/
183 | } /*for*/
184 | } /* if*/
185 | } /*for*/
186 | }
187 | PickerList.notifyDataSetChanged();
188 | ((android.widget.Button)findViewById(R.id.item_select)).setOnClickListener
189 | (
190 | new View.OnClickListener()
191 | {
192 | public void onClick
193 | (
194 | View TheView
195 | )
196 | {
197 | PickerItem Selected = null;
198 | for (int i = 0;;)
199 | {
200 | if (i == PickerList.getCount())
201 | break;
202 | final PickerItem ThisItem =
203 | (PickerItem)PickerListView.getItemAtPosition(i);
204 | if (ThisItem.Selected)
205 | {
206 | Selected = ThisItem;
207 | break;
208 | } /*if*/
209 | ++i;
210 | } /*for*/
211 | if (Selected != null)
212 | {
213 | setResult
214 | (
215 | android.app.Activity.RESULT_OK,
216 | new android.content.Intent()
217 | .setData
218 | (
219 | android.net.Uri.fromFile
220 | (
221 | new java.io.File(Selected.FullPath)
222 | )
223 | )
224 | );
225 | finish();
226 | } /*if*/
227 | } /*onClick*/
228 | } /*OnClickListener*/
229 | );
230 | } /*onCreate*/
231 |
232 | } /*Picker*/
233 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/src/GeomBuilder.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.GLUseful;
2 | /*
3 | Easy construction and application of buffers needed for
4 | OpenGL-ES drawing.
5 |
6 | Copyright 2011, 2013 by Lawrence D'Oliveiro .
7 |
8 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
9 | use this file except in compliance with the License. You may obtain a copy of
10 | the License at
11 |
12 | http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 | License for the specific language governing permissions and limitations under
18 | the License.
19 | */
20 |
21 | import java.util.ArrayList;
22 | import java.nio.ByteBuffer;
23 | import java.nio.IntBuffer;
24 | import java.nio.ShortBuffer;
25 | import java.nio.ByteOrder;
26 | import static nz.gen.geek_central.GLUseful.GLUseful.gl;
27 |
28 | public class GeomBuilder
29 | /*
30 | Helper class for easier construction of geometrical
31 | objects. Instantiate this and tell it whether each vertex will
32 | also have a normal vector, a texture-coordinate vector or a
33 | colour. Then call Add to add vertex definitions (using class Vec3f
34 | to define points, and GeomBuilder.Color to define colours), and
35 | use the returned vertex indices to construct faces with AddTri and
36 | AddQuad. Finally, call MakeObj to obtain a GeomBuilder.Obj that
37 | has a Draw method that will render the resulting geometry into a
38 | specified GL context.
39 | */
40 | {
41 |
42 | public static class Color
43 | /* RGB colours with transparency */
44 | {
45 | public final float r, g, b, a;
46 |
47 | public Color
48 | (
49 | float r,
50 | float g,
51 | float b,
52 | float a
53 | )
54 | {
55 | this.r = r;
56 | this.b = b;
57 | this.g = g;
58 | this.a = a;
59 | } /*Color*/
60 |
61 | } /*Color*/
62 |
63 | private final boolean AutoNormals;
64 | private final ArrayList TempPoints, TempPointTexCoords;
65 | private final ArrayList TempPointColors;
66 | private final ArrayList Points, PointNormals, PointTexCoords;
67 | private final ArrayList PointColors;
68 | private final ArrayList Faces;
69 | private Vec3f BoundMin, BoundMax;
70 |
71 | public GeomBuilder
72 | (
73 | boolean GotNormals, /* vertices will have normals specified, otherwise they will be automatically generated for flat shading */
74 | boolean GotTexCoords, /* vertices will have texture coordinates specified */
75 | boolean GotColors /* vertices will have colours specified */
76 | )
77 | {
78 | AutoNormals = !GotNormals;
79 | TempPoints = AutoNormals ? new ArrayList() : null;
80 | Points = new ArrayList();
81 | PointNormals = GotNormals || AutoNormals ? new ArrayList() : null;
82 | TempPointTexCoords = AutoNormals && GotTexCoords ? new ArrayList() : null;
83 | PointTexCoords = GotTexCoords ? new ArrayList() : null;
84 | TempPointColors = AutoNormals && GotColors ? new ArrayList() : null;
85 | PointColors = GotColors ? new ArrayList() : null;
86 | Faces = new ArrayList();
87 | BoundMin = null;
88 | BoundMax = null;
89 | } /*GeomBuilder*/
90 |
91 | private int Add
92 | (
93 | Vec3f Vertex,
94 | /* following args are either mandatory or must be null, depending
95 | on respective flags passed to constructor */
96 | Vec3f Normal,
97 | Vec3f TexCoord,
98 | Color VertexColor,
99 | boolean AutoNormals
100 | )
101 | /* adds a new vertex, and returns its index for use in constructing faces. */
102 | {
103 | if
104 | (
105 | AutoNormals != (Normal == null)
106 | ||
107 | (PointColors == null) != (VertexColor == null)
108 | ||
109 | (PointTexCoords == null) != (TexCoord == null)
110 | )
111 | {
112 | throw new RuntimeException("missing or redundant args specified");
113 | } /*if*/
114 | final int Result = AutoNormals ? TempPoints.size() : Points.size();
115 | (AutoNormals ? TempPoints : Points).add(Vertex);
116 | if (!AutoNormals)
117 | {
118 | PointNormals.add(Normal);
119 | } /*if*/
120 | if (PointTexCoords != null)
121 | {
122 | (AutoNormals ? TempPointTexCoords : PointTexCoords).add(TexCoord);
123 | } /*if*/
124 | if (PointColors != null)
125 | {
126 | (AutoNormals ? TempPointColors : PointColors).add(VertexColor);
127 | } /*if*/
128 | if (AutoNormals == this.AutoNormals)
129 | {
130 | if (BoundMin != null)
131 | {
132 | BoundMin =
133 | new Vec3f
134 | (
135 | Math.min(BoundMin.x, Vertex.x),
136 | Math.min(BoundMin.y, Vertex.y),
137 | Math.min(BoundMin.z, Vertex.z)
138 | );
139 | }
140 | else
141 | {
142 | BoundMin = Vertex;
143 | } /*if*/
144 | if (BoundMax != null)
145 | {
146 | BoundMax =
147 | new Vec3f
148 | (
149 | Math.max(BoundMax.x, Vertex.x),
150 | Math.max(BoundMax.y, Vertex.y),
151 | Math.max(BoundMax.z, Vertex.z)
152 | );
153 | }
154 | else
155 | {
156 | BoundMax = Vertex;
157 | } /*if*/
158 | } /*if*/
159 | return
160 | Result;
161 | } /*Add*/
162 |
163 | public int Add
164 | (
165 | Vec3f Vertex,
166 | /* following args are either mandatory or must be null, depending
167 | on respective flags passed to constructor */
168 | Vec3f Normal,
169 | Vec3f TexCoord,
170 | Color VertexColor
171 | )
172 | /* adds a new vertex, and returns its index for use in constructing faces. */
173 | {
174 | return
175 | Add
176 | (
177 | /*Vertex =*/ Vertex,
178 | /*Normal =*/ Normal,
179 | /*TexCoord =*/ TexCoord,
180 | /*VertexColor =*/ VertexColor,
181 | /*AutoNormals =*/ AutoNormals
182 | );
183 | } /*Add*/
184 |
185 | private int AddActual
186 | (
187 | int VertIndex,
188 | Vec3f Normal
189 | )
190 | {
191 | if (!AutoNormals)
192 | {
193 | throw new RuntimeException("GeomBuilder.AddActual shouldn’t be called if not AutoNormals");
194 | } /*if*/
195 | return
196 | Add
197 | (
198 | /*Vertex =*/ TempPoints.get(VertIndex),
199 | /*Normal =*/ Normal,
200 | /*TexCoord =*/ TempPointTexCoords != null ? TempPointTexCoords.get(VertIndex) : null,
201 | /*VertexColor =*/ TempPointColors != null ? TempPointColors.get(VertIndex) : null,
202 | /*AutoNormals =*/ false
203 | );
204 | } /*AddActual*/
205 |
206 | private void GenAutoNormal
207 | (
208 | int[] Vertices
209 | /* assume length at least 3 and coplanar if > 3, replaced with final generated vertices */
210 | )
211 | {
212 | if (!AutoNormals)
213 | {
214 | throw new RuntimeException("GeomBuilder.GenAutoNormal shouldn’t be called if not AutoNormals");
215 | } /*if*/
216 | final Vec3f
217 | V1 = TempPoints.get(Vertices[0]),
218 | V2 = TempPoints.get(Vertices[1]),
219 | V3 = TempPoints.get(Vertices[2]);
220 | final Vec3f FaceNormal = (V2.sub(V1)).cross(V3.sub(V2)).unit();
221 | final int[] NewVertices = new int[Vertices.length];
222 | for (int i = 0; i < Vertices.length; ++i)
223 | {
224 | NewVertices[i] = AddActual(Vertices[i], FaceNormal);
225 | } /*for*/
226 | System.arraycopy(NewVertices, 0, Vertices, 0, Vertices.length);
227 | } /*GenAutoNormal*/
228 |
229 | private void AddTri
230 | (
231 | int V1,
232 | int V2,
233 | int V3,
234 | boolean AutoNormals
235 | )
236 | /* defines a triangular face. Args are indices as previously returned from calls to Add. */
237 | {
238 | if (AutoNormals)
239 | {
240 | final int[] Vertices = new int[] {V1, V2, V3};
241 | GenAutoNormal(Vertices);
242 | V1 = Vertices[0];
243 | V2 = Vertices[1];
244 | V3 = Vertices[2];
245 | } /*if*/
246 | Faces.add(V1);
247 | Faces.add(V2);
248 | Faces.add(V3);
249 | } /*AddTri*/
250 |
251 | public void AddTri
252 | (
253 | int V1,
254 | int V2,
255 | int V3
256 | )
257 | /* defines a triangular face. Args are indices as previously returned from calls to Add. */
258 | {
259 | AddTri(V1, V2, V3, AutoNormals);
260 | } /*AddTri*/
261 |
262 | public void AddQuad
263 | (
264 | int V1,
265 | int V2,
266 | int V3,
267 | int V4
268 | )
269 | /* Defines a quadrilateral face. Args are indices as previously returned from calls to Add. */
270 | {
271 | if (AutoNormals)
272 | {
273 | final int[] Vertices = new int[] {V1, V2, V3, V4};
274 | GenAutoNormal(Vertices);
275 | V1 = Vertices[0];
276 | V2 = Vertices[1];
277 | V3 = Vertices[2];
278 | V4 = Vertices[3];
279 | } /*if*/
280 | AddTri(V1, V2, V3, false);
281 | AddTri(V4, V1, V3, false);
282 | } /*AddQuad*/
283 |
284 | public void AddPoly
285 | (
286 | int[] V
287 | )
288 | /* Defines a polygonal face. Array elements are indices as previously
289 | returned from calls to Add. */
290 | {
291 | if (AutoNormals)
292 | {
293 | final int[] V2 = new int[V.length];
294 | System.arraycopy(V, 0, V2, 0, V.length);
295 | GenAutoNormal(V2);
296 | V = V2;
297 | } /*if*/
298 | for (int i = 1; i < V.length - 1; ++i)
299 | {
300 | AddTri(V[0], V[i], V[i + 1], false);
301 | } /*for*/
302 | } /*AddPoly*/
303 |
304 | public static class Obj
305 | /* representation of complete object geometry. */
306 | {
307 | private final IntBuffer VertexBuffer;
308 | private final IntBuffer NormalBuffer;
309 | private final IntBuffer TexCoordBuffer;
310 | private final IntBuffer ColorBuffer;
311 | private final ShortBuffer IndexBuffer;
312 | private final int NrIndexes;
313 | public final Vec3f BoundMin, BoundMax;
314 |
315 | private Obj
316 | (
317 | IntBuffer VertexBuffer,
318 | IntBuffer NormalBuffer, /* optional */
319 | IntBuffer TexCoordBuffer, /* optional */
320 | IntBuffer ColorBuffer, /* optional */
321 | ShortBuffer IndexBuffer,
322 | int NrIndexes,
323 | Vec3f BoundMin,
324 | Vec3f BoundMax
325 | )
326 | {
327 | this.VertexBuffer = VertexBuffer;
328 | this.NormalBuffer = NormalBuffer;
329 | this.TexCoordBuffer = TexCoordBuffer;
330 | this.ColorBuffer = ColorBuffer;
331 | this.IndexBuffer = IndexBuffer;
332 | this.NrIndexes = NrIndexes;
333 | this.BoundMin = BoundMin;
334 | this.BoundMax = BoundMax;
335 | } /*Obj*/
336 |
337 | public void Draw()
338 | /* actually renders the geometry into the specified GL context. */
339 | {
340 | gl.glEnableClientState(gl.GL_VERTEX_ARRAY);
341 | gl.glVertexPointer(3, gl.GL_FIXED, 0, VertexBuffer);
342 | if (NormalBuffer != null)
343 | {
344 | gl.glEnableClientState(gl.GL_NORMAL_ARRAY);
345 | gl.glNormalPointer(gl.GL_FIXED, 0, NormalBuffer);
346 | } /*if*/
347 | if (TexCoordBuffer != null)
348 | {
349 | gl.glEnableClientState(gl.GL_TEXTURE_COORD_ARRAY);
350 | gl.glTexCoordPointer(3, gl.GL_FIXED, 0, TexCoordBuffer);
351 | } /*if*/
352 | if (ColorBuffer != null)
353 | {
354 | gl.glEnableClientState(gl.GL_COLOR_ARRAY);
355 | gl.glColorPointer(4, gl.GL_FIXED, 0, ColorBuffer);
356 | } /*if*/
357 | gl.glDrawElements(gl.GL_TRIANGLES, NrIndexes, gl.GL_UNSIGNED_SHORT, IndexBuffer);
358 | gl.glDisableClientState(gl.GL_VERTEX_ARRAY);
359 | gl.glDisableClientState(gl.GL_NORMAL_ARRAY);
360 | gl.glDisableClientState(gl.GL_TEXTURE_COORD_ARRAY);
361 | gl.glDisableClientState(gl.GL_COLOR_ARRAY);
362 | } /*Draw*/
363 |
364 | } /*Obj*/;
365 |
366 | public Obj MakeObj()
367 | /* constructs and returns the final geometry ready for rendering. */
368 | {
369 | if (Points.size() == 0)
370 | {
371 | throw new RuntimeException("GeomBuilder: empty object");
372 | } /*if*/
373 | final int Fixed1 = 0x10000;
374 | final int[] Vertices = new int[Points.size() * 3];
375 | final int[] Normals = PointNormals != null ? new int[Points.size() * 3] : null;
376 | final int[] TexCoords = PointTexCoords != null ? new int[Points.size() * 3] : null;
377 | final int[] Colors = PointColors != null ? new int[Points.size() * 4] : null;
378 | int jv = 0, jn = 0, jt = 0, jc = 0;
379 | for (int i = 0; i < Points.size(); ++i)
380 | {
381 | final Vec3f Point = Points.get(i);
382 | Vertices[jv++] = (int)(Point.x * Fixed1);
383 | Vertices[jv++] = (int)(Point.y * Fixed1);
384 | Vertices[jv++] = (int)(Point.z * Fixed1);
385 | if (PointNormals != null)
386 | {
387 | final Vec3f PointNormal = PointNormals.get(i);
388 | Normals[jn++] = (int)(PointNormal.x * Fixed1);
389 | Normals[jn++] = (int)(PointNormal.y * Fixed1);
390 | Normals[jn++] = (int)(PointNormal.z * Fixed1);
391 | } /*if*/
392 | if (PointTexCoords != null)
393 | {
394 | final Vec3f Coord = PointTexCoords.get(i);
395 | TexCoords[jt++] = (int)(Coord.x * Fixed1);
396 | TexCoords[jt++] = (int)(Coord.y * Fixed1);
397 | TexCoords[jt++] = (int)(Coord.z * Fixed1);
398 | } /*if*/
399 | if (PointColors != null)
400 | {
401 | final Color ThisColor = PointColors.get(i);
402 | Colors[jc++] = (int)(ThisColor.r * Fixed1);
403 | Colors[jc++] = (int)(ThisColor.g * Fixed1);
404 | Colors[jc++] = (int)(ThisColor.b * Fixed1);
405 | Colors[jc++] = (int)(ThisColor.a * Fixed1);
406 | } /*if*/
407 | } /*for*/
408 | final short[] Indices = new short[Faces.size()];
409 | final int NrIndexes = Indices.length;
410 | for (int i = 0; i < NrIndexes; ++i)
411 | {
412 | Indices[i] = (short)(int)Faces.get(i);
413 | } /*for*/
414 | /* Need to use allocateDirect to allocate buffers so garbage
415 | collector won't move them. Also make sure byte order is
416 | always native. But direct-allocation and order-setting methods
417 | are only available for ByteBuffer. Which is why buffers
418 | are allocated as ByteBuffers and then converted to more
419 | appropriate types. */
420 | final IntBuffer VertexBuffer;
421 | final IntBuffer NormalBuffer;
422 | final IntBuffer TexCoordBuffer;
423 | final IntBuffer ColorBuffer;
424 | final ShortBuffer IndexBuffer;
425 | VertexBuffer =
426 | ByteBuffer.allocateDirect(Vertices.length * 4)
427 | .order(ByteOrder.nativeOrder())
428 | .asIntBuffer()
429 | .put(Vertices);
430 | VertexBuffer.position(0);
431 | if (PointNormals != null)
432 | {
433 | NormalBuffer =
434 | ByteBuffer.allocateDirect(Normals.length * 4)
435 | .order(ByteOrder.nativeOrder())
436 | .asIntBuffer()
437 | .put(Normals);
438 | NormalBuffer.position(0);
439 | }
440 | else
441 | {
442 | NormalBuffer = null;
443 | } /*if*/
444 | if (PointTexCoords != null)
445 | {
446 | TexCoordBuffer =
447 | ByteBuffer.allocateDirect(TexCoords.length * 4)
448 | .order(ByteOrder.nativeOrder())
449 | .asIntBuffer()
450 | .put(TexCoords);
451 | TexCoordBuffer.position(0);
452 | }
453 | else
454 | {
455 | TexCoordBuffer = null;
456 | } /*if*/
457 | if (PointColors != null)
458 | {
459 | ColorBuffer =
460 | ByteBuffer.allocateDirect(Colors.length * 4)
461 | .order(ByteOrder.nativeOrder())
462 | .asIntBuffer()
463 | .put(Colors);
464 | ColorBuffer.position(0);
465 | }
466 | else
467 | {
468 | ColorBuffer = null;
469 | } /*if*/
470 | IndexBuffer =
471 | ByteBuffer.allocateDirect(Indices.length * 2)
472 | .order(ByteOrder.nativeOrder())
473 | .asShortBuffer()
474 | .put(Indices);
475 | IndexBuffer.position(0);
476 | return
477 | new Obj
478 | (
479 | VertexBuffer,
480 | NormalBuffer,
481 | TexCoordBuffer,
482 | ColorBuffer,
483 | IndexBuffer,
484 | NrIndexes,
485 | BoundMin,
486 | BoundMax
487 | );
488 | } /*MakeObj*/
489 |
490 | } /*GeomBuilder*/;
491 |
--------------------------------------------------------------------------------
/src/Main.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.ObjViewer;
2 | /*
3 | ObjViewer -- viewer for .obj files -- mainline.
4 |
5 | Copyright 2011-2014 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | import nz.gen.geek_central.GLUseful.ObjReader;
21 |
22 | public class Main extends android.app.Activity
23 | {
24 | java.util.Map OptionsMenu;
25 |
26 | ObjectView TheObjectView;
27 |
28 | final java.security.SecureRandom Random = new java.security.SecureRandom();
29 | /* why use anything less */
30 |
31 | float Rand()
32 | /* returns a random float in [0.0 .. 1.0). */
33 | {
34 | final byte[] V = new byte[4];
35 | Random.nextBytes(V);
36 | return
37 | (float)(
38 | ((int)V[0] & 255)
39 | |
40 | ((int)V[1] & 255) << 8
41 | |
42 | ((int)V[2] & 255) << 16
43 | |
44 | ((int)V[3] & 255) << 24
45 | )
46 | /
47 | 4294967296.0f;
48 | } /*Rand*/
49 |
50 | /* request codes, all arbitrarily assigned */
51 | static final int LoadObjectRequest = 1;
52 |
53 | interface RequestResponseAction /* response to an activity result */
54 | {
55 | public void Run
56 | (
57 | int ResultCode,
58 | android.content.Intent Data
59 | );
60 | } /*RequestResponseAction*/
61 |
62 | java.util.Map ActivityResultActions;
63 |
64 | private interface SelectedIDAction
65 | {
66 | public void Set
67 | (
68 | int SelectedID
69 | );
70 | } /*SelectedIDAction*/
71 |
72 | private ObjReader.Model ReadObj
73 | (
74 | final String ObjFileName
75 | )
76 | {
77 | ObjReader.Model Result = null;
78 | try
79 | {
80 | Result = ObjReader.ReadObj
81 | (
82 | /*FileName =*/ ObjFileName,
83 | /*LoadMaterials =*/
84 | new ObjReader.MaterialLoader()
85 | {
86 | public ObjReader.MaterialSet Load
87 | (
88 | ObjReader.MaterialSet Materials,
89 | String MatFileName
90 | )
91 | {
92 | return
93 | ObjReader.ReadMaterials
94 | (
95 | /*FileName =*/
96 | new java.io.File
97 | (
98 | new java.io.File(ObjFileName)
99 | .getParentFile(),
100 | MatFileName
101 | ).getPath(),
102 | /*CurMaterials =*/ Materials
103 | );
104 | } /*Load*/
105 | } /*MaterialLoader*/
106 | );
107 | }
108 | catch (ObjReader.DataFormatException Failed)
109 | {
110 | android.widget.Toast.makeText
111 | (
112 | /*context =*/ this,
113 | /*text =*/
114 | String.format
115 | (
116 | nz.gen.geek_central.GLUseful.GLUseful.StdLocale,
117 | getString(R.string.obj_load_fail),
118 | Failed.toString()
119 | ),
120 | /*duration =*/ android.widget.Toast.LENGTH_SHORT
121 | ).show();
122 | } /*try*/
123 | return
124 | Result;
125 | } /*ReadObj*/
126 |
127 | private String CurObjFileName = null;
128 | private static final String CurFileKey = "curfile";
129 |
130 | private class OptionsDialog
131 | extends android.app.Dialog
132 | implements android.content.DialogInterface.OnDismissListener
133 | {
134 | private final android.content.Context ctx;
135 | private final String Title;
136 | private final SelectedIDAction Action;
137 | private final int InitialButtonID;
138 | private class ButtonDef
139 | {
140 | final String ButtonTitle;
141 | final int ButtonID;
142 |
143 | public ButtonDef
144 | (
145 | String ButtonTitle,
146 | int ButtonID
147 | )
148 | {
149 | this.ButtonTitle = ButtonTitle;
150 | this.ButtonID = ButtonID;
151 | } /*ButtonDef*/
152 | } /*ButtonDef*/
153 | private final java.util.ArrayList TheButtonDefs =
154 | new java.util.ArrayList();
155 | private android.widget.RadioGroup TheButtons;
156 |
157 | public OptionsDialog
158 | (
159 | android.content.Context ctx,
160 | String Title,
161 | SelectedIDAction Action,
162 | int InitialButtonID
163 | )
164 | {
165 | super(ctx);
166 | this.ctx = ctx;
167 | this.Title = Title;
168 | this.Action = Action;
169 | this.InitialButtonID = InitialButtonID;
170 | } /*OptionsDialog*/
171 |
172 | public OptionsDialog AddButton
173 | (
174 | String ButtonTitle,
175 | int ButtonID
176 | )
177 | {
178 | TheButtonDefs.add(new ButtonDef(ButtonTitle, ButtonID));
179 | return
180 | this;
181 | } /*AddButton*/
182 |
183 | @Override
184 | public void onCreate
185 | (
186 | android.os.Bundle ToRestore
187 | )
188 | {
189 | setTitle(Title);
190 | final android.widget.LinearLayout MainLayout = new android.widget.LinearLayout(ctx);
191 | MainLayout.setOrientation(android.widget.LinearLayout.VERTICAL);
192 | setContentView(MainLayout);
193 | TheButtons = new android.widget.RadioGroup(ctx);
194 | final android.view.ViewGroup.LayoutParams ButtonLayout =
195 | new android.view.ViewGroup.LayoutParams
196 | (
197 | android.view.ViewGroup.LayoutParams.FILL_PARENT,
198 | android.view.ViewGroup.LayoutParams.WRAP_CONTENT
199 | );
200 | for (ButtonDef ThisButtonDef : TheButtonDefs)
201 | {
202 | final android.widget.RadioButton ThisButton =
203 | new android.widget.RadioButton(ctx);
204 | ThisButton.setText(ThisButtonDef.ButtonTitle);
205 | ThisButton.setId(ThisButtonDef.ButtonID);
206 | TheButtons.addView(ThisButton, TheButtons.getChildCount(), ButtonLayout);
207 | } /*for*/
208 | MainLayout.addView(TheButtons, ButtonLayout);
209 | TheButtons.check(InitialButtonID);
210 | setOnDismissListener(this);
211 | } /*onCreate*/
212 |
213 | @Override
214 | public void onDismiss
215 | (
216 | android.content.DialogInterface TheDialog
217 | )
218 | {
219 | Action.Set(TheButtons.getCheckedRadioButtonId());
220 | } /*onDismiss*/
221 |
222 | } /*OptionsDialog*/
223 |
224 | @Override
225 | public boolean onCreateOptionsMenu
226 | (
227 | android.view.Menu TheMenu
228 | )
229 | {
230 | OptionsMenu = new java.util.HashMap();
231 | OptionsMenu.put
232 | (
233 | TheMenu.add(R.string.pick_file),
234 | new Runnable()
235 | {
236 | public void run()
237 | {
238 | startActivityForResult
239 | (
240 | new android.content.Intent(android.content.Intent.ACTION_PICK)
241 | .setClass(Main.this, Picker.class)
242 | .putExtra(Picker.ExtensionID, ".obj")
243 | .putExtra
244 | (
245 | Picker.LookInID,
246 | new String[]
247 | {
248 | "Models",
249 | "Download",
250 | }
251 | ),
252 | LoadObjectRequest
253 | );
254 | } /*run*/
255 | } /*Runnable*/
256 | );
257 | OptionsMenu.put
258 | (
259 | TheMenu.add(R.string.reset_view),
260 | new Runnable()
261 | {
262 | public void run()
263 | {
264 | TheObjectView.ResetOrientation(true);
265 | } /*run*/
266 | } /*Runnable*/
267 | );
268 | OptionsMenu.put
269 | (
270 | TheMenu.add(R.string.options_lighting),
271 | new Runnable()
272 | {
273 | public void run()
274 | {
275 | new OptionsDialog
276 | (
277 | /*ctx =*/ Main.this,
278 | /*Title =*/ getString(R.string.lighting_title),
279 | /*Action =*/
280 | new SelectedIDAction()
281 | {
282 | public void Set
283 | (
284 | int SelectedID
285 | )
286 | {
287 | TheObjectView.SetUseLighting(SelectedID != 0);
288 | } /*Set*/
289 | } /*SelectedIDAction*/,
290 | /*InitialButtonID =*/ TheObjectView.GetUseLighting() ? 1 : 0
291 | )
292 | .AddButton(getString(R.string.on), 1)
293 | .AddButton(getString(R.string.off), 0)
294 | .show();
295 | } /*run*/
296 | } /*Runnable*/
297 | );
298 | OptionsMenu.put
299 | (
300 | TheMenu.add(R.string.options_orient_faces),
301 | new Runnable()
302 | {
303 | public void run()
304 | {
305 | new OptionsDialog
306 | (
307 | /*ctx =*/ Main.this,
308 | /*Title =*/ getString(R.string.orient_faces_title),
309 | /*Action =*/
310 | new SelectedIDAction()
311 | {
312 | public void Set
313 | (
314 | int SelectedID
315 | )
316 | {
317 | TheObjectView.SetClockwiseFaces(SelectedID != 0);
318 | } /*Set*/
319 | } /*SelectedIDAction*/,
320 | /*InitialButtonID =*/ TheObjectView.GetClockwiseFaces() ? 1 : 0
321 | )
322 | .AddButton(getString(R.string.anticlockwise), 0)
323 | .AddButton(getString(R.string.clockwise), 1)
324 | .show();
325 | } /*run*/
326 | } /*Runnable*/
327 | );
328 | OptionsMenu.put
329 | (
330 | TheMenu.add(R.string.random_view),
331 | new Runnable()
332 | {
333 | public void run()
334 | {
335 | final float
336 | X = Rand(),
337 | Y = Rand(),
338 | Z = Rand(),
339 | R = (float)Math.sqrt(X * X + Y * Y + Z * Z),
340 | Angle = Rand() * 360.0f;
341 | TheObjectView.SetOrientation
342 | (
343 | new nz.gen.geek_central.GLUseful.Rotation(Angle, true, X / R, Y / R, Z / R),
344 | true
345 | );
346 | } /*run*/
347 | } /*Runnable*/
348 | );
349 | return
350 | true;
351 | } /*onCreateOptionsMenu*/
352 |
353 | void BuildActivityResultActions()
354 | {
355 | ActivityResultActions = new java.util.HashMap();
356 | ActivityResultActions.put
357 | (
358 | LoadObjectRequest,
359 | new RequestResponseAction()
360 | {
361 | public void Run
362 | (
363 | int ResultCode,
364 | android.content.Intent Data
365 | )
366 | {
367 | /* Unfortunately I can't send this Intent directly from the Picker
368 | without using some odd launch-mode settings to avoid another instance
369 | of Main being created. Which is why I do it here. */
370 | startActivity
371 | (
372 | new android.content.Intent(android.content.Intent.ACTION_VIEW, Data.getData())
373 | .setClass(Main.this, Main.class)
374 | .setFlags(android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP)
375 | );
376 | } /*Run*/
377 | } /*RequestResponseAction*/
378 | );
379 | } /*BuildActivityResultActions*/
380 |
381 | @Override
382 | public void onCreate
383 | (
384 | android.os.Bundle ToRestore
385 | )
386 | {
387 | super.onCreate(ToRestore);
388 | setContentView(R.layout.main);
389 | TheObjectView = (ObjectView)findViewById(R.id.object_view);
390 | BuildActivityResultActions();
391 | final ObjReader.Model PreviousModel = (ObjReader.Model)getLastNonConfigurationInstance();
392 | if (PreviousModel != null)
393 | {
394 | TheObjectView.SetObject(PreviousModel);
395 | } /*if*/
396 | if (ToRestore != null)
397 | {
398 | /* reload previously-viewed object */
399 | CurObjFileName = ToRestore.getString(CurFileKey);
400 | if (PreviousModel == null && CurObjFileName != null)
401 | {
402 | TheObjectView.SetObject(ReadObj(CurObjFileName));
403 | } /*if*/
404 | TheObjectView.onRestoreInstanceState(ToRestore.getParcelable("ObjectView"));
405 | /* doesn't seem to be done by GLSurfaceView */
406 | }
407 | else
408 | {
409 | android.widget.Toast.makeText
410 | (
411 | /*context =*/ this,
412 | /*text =*/ getString(R.string.startup_prompt),
413 | /*duration =*/ android.widget.Toast.LENGTH_SHORT
414 | ).show();
415 | } /*if*/
416 | onNewIntent(getIntent());
417 | } /*onCreate*/
418 |
419 | @Override
420 | protected void onNewIntent
421 | (
422 | android.content.Intent TheIntent
423 | )
424 | {
425 | String Action = TheIntent.getAction();
426 | if (Action != null)
427 | {
428 | Action = Action.intern();
429 | } /*if*/
430 | if (Action == android.content.Intent.ACTION_VIEW)
431 | {
432 | final String ObjFileName = TheIntent.getData().getPath();
433 | final ObjReader.Model NewObj = ReadObj(ObjFileName);
434 | if (NewObj != null)
435 | {
436 | CurObjFileName = ObjFileName;
437 | TheObjectView.SetObject(NewObj);
438 | } /*if*/
439 | } /*if*/
440 | } /*onnewIntent*/
441 |
442 | @Override
443 | public void onPause()
444 | {
445 | super.onPause();
446 | TheObjectView.onPause();
447 | } /*onPause*/
448 |
449 | @Override
450 | public void onResume()
451 | {
452 | super.onResume();
453 | TheObjectView.onResume();
454 | } /*onResume*/
455 |
456 | @Override
457 | public Object onRetainNonConfigurationInstance()
458 | /* optimization to avoid re-reading .obj file */
459 | {
460 | return
461 | TheObjectView.GetObject();
462 | } /*onRetainNonConfigurationInstance*/
463 |
464 | @Override
465 | public void onSaveInstanceState
466 | (
467 | android.os.Bundle ToRestore
468 | )
469 | {
470 | if (CurObjFileName != null)
471 | {
472 | /* remember what file I was looking at */
473 | ToRestore.putString(CurFileKey, CurObjFileName);
474 | } /*if*/
475 | ToRestore.putParcelable("ObjectView", TheObjectView.onSaveInstanceState());
476 | /* doesn't seem to be done by GLSurfaceView */
477 | } /*onSaveInstanceState*/
478 |
479 | @Override
480 | public boolean onOptionsItemSelected
481 | (
482 | android.view.MenuItem TheItem
483 | )
484 | {
485 | boolean Handled = false;
486 | final Runnable Action = OptionsMenu.get(TheItem);
487 | if (Action != null)
488 | {
489 | Action.run();
490 | Handled = true;
491 | } /*if*/
492 | return
493 | Handled;
494 | } /*onOptionsItemSelected*/
495 |
496 | @Override
497 | public void onActivityResult
498 | (
499 | int RequestCode,
500 | int ResultCode,
501 | android.content.Intent Data
502 | )
503 | {
504 | System.err.printf("ObjViewer.onActivityResult request %d result %d\n", RequestCode, ResultCode); /* debug */
505 | if (ResultCode != android.app.Activity.RESULT_CANCELED)
506 | {
507 | final RequestResponseAction Action = ActivityResultActions.get(RequestCode);
508 | if (Action != null)
509 | {
510 | Action.Run(ResultCode, Data);
511 | } /*if*/
512 | } /*if*/
513 | } /*onActivityResult*/
514 |
515 | } /*Main*/
516 |
--------------------------------------------------------------------------------
/src/ObjectView.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.ObjViewer;
2 | /*
3 | 3D view widget. Lets the user apply interactive rotation of the
4 | object around any axis. Also does rotation animations.
5 |
6 | Copyright 2011, 2013 by Lawrence D'Oliveiro .
7 |
8 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
9 | use this file except in compliance with the License. You may obtain a copy of
10 | the License at
11 |
12 | http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 | License for the specific language governing permissions and limitations under
18 | the License.
19 | */
20 |
21 | import javax.microedition.khronos.opengles.GL10;
22 | import nz.gen.geek_central.GLUseful.ObjReader;
23 | import nz.gen.geek_central.GLUseful.Rotation;
24 | import android.graphics.PointF;
25 | import android.view.MotionEvent;
26 | import static nz.gen.geek_central.android.useful.Useful.GetTime;
27 | import nz.gen.geek_central.android.useful.BundledSavedState;
28 | import static nz.gen.geek_central.GLUseful.GLUseful.gl;
29 |
30 | public class ObjectView extends android.opengl.GLSurfaceView
31 | {
32 | private static boolean DefaultUseLighting = true;
33 | private static boolean DefaultClockwiseFaces = false;
34 | private static Rotation DefaultRotation = Rotation.Null;
35 |
36 | private ObjReader.Model TheObject = null;
37 | private boolean UseLighting = DefaultUseLighting;
38 | private boolean ClockwiseFaces = DefaultClockwiseFaces;
39 | private Rotation CurRotation = DefaultRotation;
40 | private PointF LastMouse = null;
41 |
42 | private class ObjectViewRenderer implements Renderer
43 | {
44 | /* Note I ignore the passed GL10 argument, and exclusively use
45 | static methods from GLES11 class for all OpenGL drawing, since
46 | this seems to be the preferred way */
47 |
48 | public ObjectViewRenderer()
49 | {
50 | super();
51 | /* nothing else to do, really */
52 | } /*ObjectViewRenderer*/
53 |
54 | public void onDrawFrame
55 | (
56 | GL10 _gl
57 | )
58 | {
59 | gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT);
60 | gl.glMatrixMode(gl.GL_MODELVIEW);
61 | gl.glLoadIdentity();
62 | if (TheObject != null)
63 | {
64 | if (UseLighting)
65 | {
66 | gl.glEnable(gl.GL_LIGHTING);
67 | gl.glLightModelfv
68 | (
69 | /*pname =*/ gl.GL_LIGHT_MODEL_AMBIENT,
70 | /*params =*/ new float[] {0.3f, 0.3f, 0.3f, 1.0f},
71 | /*offset =*/ 0
72 | ); /* so hopefully objects are never completely black */
73 | /* light positions are fixed relative to view */
74 | gl.glEnable(gl.GL_LIGHT0);
75 | gl.glLightfv
76 | (
77 | /*light =*/ gl.GL_LIGHT0,
78 | /*pname =*/ gl.GL_POSITION,
79 | /*params =*/ new float[] {0.0f, 2.0f, 0.0f, 1.0f},
80 | /*offset =*/ 0
81 | );
82 | gl.glLightfv
83 | (
84 | /*light =*/ gl.GL_LIGHT0,
85 | /*pname =*/ gl.GL_AMBIENT,
86 | /*params =*/ new float[] {0.4f, 0.4f, 0.4f, 1.0f},
87 | /*offset =*/ 0
88 | );
89 | gl.glLightfv
90 | (
91 | /*light =*/ gl.GL_LIGHT0,
92 | /*pname =*/ gl.GL_DIFFUSE,
93 | /*params =*/ new float[] {0.7f, 0.7f, 0.7f, 1.0f},
94 | /*offset =*/ 0
95 | );
96 | gl.glLightfv
97 | (
98 | /*light =*/ gl.GL_LIGHT0,
99 | /*pname =*/ gl.GL_SPECULAR,
100 | /*params =*/ new float[] {0.7f, 0.7f, 0.7f, 1.0f},
101 | /*offset =*/ 0
102 | );
103 | gl.glEnable(gl.GL_LIGHT1);
104 | gl.glLightfv
105 | (
106 | /*light =*/ gl.GL_LIGHT1,
107 | /*pname =*/ gl.GL_POSITION,
108 | /*params =*/ new float[] {0.0f, 0.0f, -2.0f, 1.0f},
109 | /*offset =*/ 0
110 | );
111 | gl.glLightfv
112 | (
113 | /*light =*/ gl.GL_LIGHT1,
114 | /*pname =*/ gl.GL_DIFFUSE,
115 | /*params =*/ new float[] {0.3f, 0.3f, 0.3f, 1.0f},
116 | /*offset =*/ 0
117 | );
118 | gl.glLightfv
119 | (
120 | /*light =*/ gl.GL_LIGHT1,
121 | /*pname =*/ gl.GL_SPECULAR,
122 | /*params =*/ new float[] {0.3f, 0.3f, 0.3f, 1.0f},
123 | /*offset =*/ 0
124 | );
125 | }
126 | else
127 | {
128 | gl.glDisable(gl.GL_LIGHTING);
129 | } /*if*/
130 | final float MaxDim =
131 | Math.max
132 | (
133 | Math.max
134 | (
135 | TheObject.BoundMax.x - TheObject.BoundMin.x,
136 | TheObject.BoundMax.y - TheObject.BoundMin.y
137 | ),
138 | TheObject.BoundMax.z - TheObject.BoundMin.z
139 | );
140 | final float Scale = 2.5f;
141 | gl.glTranslatef(0.0f, 0.0f, -2.5f);
142 | CurRotation.Apply();
143 | gl.glScalef(Scale / MaxDim, Scale / MaxDim, Scale / MaxDim);
144 | gl.glTranslatef
145 | (
146 | - (TheObject.BoundMax.x + TheObject.BoundMin.x) / 2.0f,
147 | - (TheObject.BoundMax.y + TheObject.BoundMin.y) / 2.0f,
148 | - (TheObject.BoundMax.z + TheObject.BoundMin.z) / 2.0f
149 | );
150 | gl.glFrontFace
151 | (
152 | ClockwiseFaces ?
153 | gl.GL_CW
154 | :
155 | gl.GL_CCW
156 | );
157 | TheObject.Draw();
158 | } /*if*/
159 | } /*onDrawFrame*/
160 |
161 | public void onSurfaceChanged
162 | (
163 | GL10 _gl,
164 | int ViewWidth,
165 | int ViewHeight
166 | )
167 | {
168 | gl.glViewport(0, 0, ViewWidth, ViewHeight);
169 | gl.glMatrixMode(gl.GL_PROJECTION);
170 | gl.glLoadIdentity();
171 | gl.glFrustumf
172 | (
173 | /*l =*/ ViewWidth > ViewHeight ? - 1.0f : - (float)ViewWidth / ViewHeight,
174 | /*r =*/ ViewWidth > ViewHeight ? 1.0f : (float)ViewWidth / ViewHeight,
175 | /*b =*/ ViewWidth < ViewHeight ? -1.0f : - (float)ViewHeight / ViewWidth,
176 | /*t =*/ ViewWidth < ViewHeight ? 1.0f : (float)ViewHeight / ViewWidth,
177 | /*n =*/ 1.0f,
178 | /*f =*/ 10.0f
179 | );
180 | } /*onSurfaceChanged*/
181 |
182 | public void onSurfaceCreated
183 | (
184 | GL10 _gl,
185 | javax.microedition.khronos.egl.EGLConfig Config
186 | )
187 | {
188 | gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
189 | gl.glEnable(gl.GL_CULL_FACE);
190 | gl.glShadeModel(gl.GL_SMOOTH);
191 | gl.glEnable(gl.GL_DEPTH_TEST);
192 | } /*onSurfaceCreated*/
193 |
194 | } /*ObjectViewRenderer*/
195 |
196 | public ObjectView
197 | (
198 | android.content.Context TheContext,
199 | android.util.AttributeSet TheAttributes
200 | )
201 | {
202 | super(TheContext, TheAttributes);
203 | setRenderer(new ObjectViewRenderer());
204 | setRenderMode(RENDERMODE_WHEN_DIRTY);
205 | } /*ObjectView*/
206 |
207 | private class RotationAnimator implements Runnable
208 | {
209 | final android.view.animation.Interpolator AnimFunction;
210 | final double StartTime, EndTime;
211 | final Rotation StartRotation, DeltaRotation;
212 |
213 | public RotationAnimator
214 | (
215 | android.view.animation.Interpolator AnimFunction,
216 | double Duration,
217 | Rotation EndRotation
218 | )
219 | {
220 | this.AnimFunction = AnimFunction;
221 | this.StartTime = GetTime();
222 | this.EndTime = this.StartTime + Duration;
223 | this.StartRotation = CurRotation;
224 | this.DeltaRotation = EndRotation.mul(StartRotation.inv());
225 | CurrentAnim = this;
226 | post(this);
227 | } /*RotationAnimator*/
228 |
229 | public void run()
230 | {
231 | if (CurrentAnim == this)
232 | {
233 | final double CurrentTime = GetTime();
234 | final float AnimAmt =
235 | AnimFunction.getInterpolation((float)((CurrentTime - StartTime) / (EndTime - StartTime)));
236 | CurRotation = DeltaRotation.mul(AnimAmt).mul(StartRotation);
237 | requestRender();
238 | if (CurrentTime < EndTime)
239 | {
240 | post(this);
241 | }
242 | else
243 | {
244 | CurrentAnim = null;
245 | } /*if*/
246 | } /*if*/
247 | } /*run*/
248 |
249 | } /*RotationAnimator*/
250 |
251 | private RotationAnimator CurrentAnim = null;
252 |
253 | @Override
254 | public android.os.Parcelable onSaveInstanceState()
255 | {
256 | final android.os.Bundle MyState = new android.os.Bundle();
257 | MyState.putBoolean("UseLighting", UseLighting);
258 | MyState.putBoolean("ClockwiseFaces", ClockwiseFaces);
259 | MyState.putParcelable("CurRotation", CurRotation);
260 | return
261 | new BundledSavedState
262 | (
263 | super.onSaveInstanceState(),
264 | MyState
265 | );
266 | } /*onSaveInstanceState*/
267 |
268 | @Override
269 | public void onRestoreInstanceState
270 | (
271 | android.os.Parcelable SavedState
272 | )
273 | {
274 | super.onRestoreInstanceState(((BundledSavedState)SavedState).SuperState);
275 | final android.os.Bundle MyState = ((BundledSavedState)SavedState).MyState;
276 | UseLighting = MyState.getBoolean("UseLighting", DefaultUseLighting);
277 | ClockwiseFaces = MyState.getBoolean("ClockwiseFaces", DefaultClockwiseFaces);
278 | CurRotation = (Rotation)MyState.getParcelable("CurRotation");
279 | requestRender();
280 | } /*onRestoreInstanceState*/
281 |
282 | private Rotation CalculateNewRotation
283 | (
284 | PointF LastMouse,
285 | PointF ThisMouse,
286 | boolean Fling /* debug */
287 | )
288 | {
289 | final PointF MidPoint = new PointF(getWidth() / 2.0f, getHeight() / 2.0f);
290 | final float Radius =
291 | (float)Math.hypot(ThisMouse.x - MidPoint.x, ThisMouse.y - MidPoint.y);
292 | final float DeltaR =
293 | Radius
294 | -
295 | (float)Math.hypot(LastMouse.x - MidPoint.x, LastMouse.y - MidPoint.y);
296 | /* radial movement, for rotation about X and Y axes */
297 | final float MidDiag = (float)Math.hypot(MidPoint.x, MidPoint.y);
298 | final float MaxRot = (float)Math.PI; /* impose limit in case of wild fling gestures */
299 | final float ZAngle =
300 | Radius / MidDiag >= 0.5f ?
301 | (float)
302 | (
303 | Math.atan2
304 | (
305 | ThisMouse.y - MidPoint.y,
306 | ThisMouse.x - MidPoint.x
307 | )
308 | -
309 | Math.atan2
310 | (
311 | LastMouse.y - MidPoint.y,
312 | LastMouse.x - MidPoint.x
313 | )
314 | )
315 | : /* disable Z-rotation too close to centre where it’s too hard to control */
316 | 0.0f;
317 | final float RotationSin = DeltaR / MidDiag;
318 | /* scale rotation angle by assuming depth of
319 | axis is equal to radius of view */
320 | { /* debug */
321 | final double RotationAngle =
322 | Math.toDegrees
323 | (
324 | Math.abs(RotationSin) > 1.0f ?
325 | /* can happen with fling gestures */
326 | RotationSin * (float)Math.PI / 2.0f
327 | :
328 | (float)Math.asin(RotationSin)
329 | );
330 | System.err.printf("ObjViewer.ObjectView.CalculateNewRotation fling %s LastMouse(%.3f, %.3f) × ThisMouse(%.3f, %.3f) => rotation %.3f° about (%.3f, %.3f, 0)°\n", Fling, LastMouse.x, LastMouse.y, ThisMouse.x, ThisMouse.y, RotationAngle, (ThisMouse.y - MidPoint.y) / Radius, (ThisMouse.x - MidPoint.x) / Radius);
331 | } /* debug */
332 | return
333 | new Rotation /* X+Y axis */
334 | (
335 | Math.abs(RotationSin) > 1.0f ?
336 | /* can happen with fling gestures */
337 | Math.max(Math.min(RotationSin * (float)Math.PI / 2.0f, MaxRot), - MaxRot)
338 | :
339 | (float)Math.asin(RotationSin),
340 | false,
341 | (ThisMouse.y - MidPoint.y) / Radius,
342 | (ThisMouse.x - MidPoint.x) / Radius,
343 | 0
344 | )
345 | .mul
346 | (
347 | new Rotation(Math.max(Math.min(ZAngle, MaxRot), - MaxRot), false, 0, 0, -1) /* Z axis */
348 | )
349 | .mul
350 | (
351 | CurRotation
352 | );
353 | /* ordering of composing the new rotations doesn't matter
354 | because axes are orthogonal */
355 | } /*CalculateNewRotation*/
356 |
357 | private final android.view.GestureDetector FlingDetector =
358 | new android.view.GestureDetector
359 | (
360 | getContext(),
361 | new android.view.GestureDetector.SimpleOnGestureListener()
362 | {
363 | @Override
364 | public boolean onFling
365 | (
366 | MotionEvent DownEvent,
367 | MotionEvent UpEvent,
368 | float XVelocity,
369 | float YVelocity
370 | )
371 | {
372 | final double CurrentTime = GetTime();
373 | final float InitialAttenuate = 5.0f; /* attenuates initial speed */
374 | final float FinalAttenuate = 2.0f; /* attenuates duration of spin */
375 | final float SpinDuration =
376 | (float)Math.hypot(XVelocity, YVelocity)
377 | /
378 | (float)Math.hypot(getWidth(), getHeight())
379 | /
380 | FinalAttenuate;
381 | System.err.printf("ObjViewer.ObjectView: spin duration = %.3fs\n", SpinDuration); /* debug */
382 | final PointF LastMouse = new PointF(UpEvent.getX(), UpEvent.getY());
383 | final PointF ThisMouse =
384 | new PointF
385 | (
386 | LastMouse.x
387 | +
388 | XVelocity
389 | *
390 | SpinDuration
391 | /
392 | InitialAttenuate,
393 | LastMouse.y
394 | +
395 | YVelocity
396 | *
397 | SpinDuration
398 | /
399 | InitialAttenuate
400 | );
401 | new RotationAnimator
402 | (
403 | /*AnimFunction =*/ new android.view.animation.DecelerateInterpolator(),
404 | /*Duration =*/ SpinDuration,
405 | /*EndRotation =*/ CalculateNewRotation(LastMouse, ThisMouse, true)
406 | );
407 | return
408 | true;
409 | } /*onFling*/
410 | } /*GestureDetector.SimpleOnGestureListener*/
411 | );
412 |
413 | @Override
414 | public boolean onTouchEvent
415 | (
416 | MotionEvent TheEvent
417 | )
418 | {
419 | boolean Handled = false;
420 | if (CurrentAnim == null && FlingDetector.onTouchEvent(TheEvent))
421 | {
422 | Handled = true;
423 | } /*if*/
424 | if (!Handled)
425 | {
426 | switch (TheEvent.getAction())
427 | {
428 | case MotionEvent.ACTION_DOWN:
429 | CurrentAnim = null;
430 | LastMouse = new PointF(TheEvent.getX(), TheEvent.getY());
431 | Handled = true;
432 | break;
433 | case MotionEvent.ACTION_MOVE:
434 | if (LastMouse != null && TheObject != null)
435 | {
436 | final PointF ThisMouse = new PointF(TheEvent.getX(), TheEvent.getY());
437 | CurRotation = CalculateNewRotation(LastMouse, ThisMouse, false);
438 | LastMouse = ThisMouse;
439 | requestRender();
440 | } /*if*/
441 | Handled = true;
442 | break;
443 | case MotionEvent.ACTION_UP:
444 | LastMouse = null;
445 | Handled = true;
446 | break;
447 | } /*switch*/
448 | } /*if*/
449 | return
450 | Handled;
451 | } /*onTouchEvent*/
452 |
453 | public void ResetOrientation
454 | (
455 | boolean Animate
456 | )
457 | {
458 | SetOrientation(Rotation.Null, Animate);
459 | } /*ResetOrientation*/
460 |
461 | public void SetObject
462 | (
463 | ObjReader.Model NewObject
464 | )
465 | {
466 | TheObject = NewObject;
467 | ResetOrientation(false);
468 | } /*SetObject*/
469 |
470 | public ObjReader.Model GetObject()
471 | {
472 | return
473 | TheObject;
474 | } /*GetObject*/
475 |
476 | public void SetUseLighting
477 | (
478 | boolean UseLighting
479 | )
480 | {
481 | this.UseLighting = UseLighting;
482 | requestRender();
483 | } /*SetUseLighting*/
484 |
485 | public boolean GetUseLighting()
486 | {
487 | return
488 | UseLighting;
489 | } /*GetUseLighting*/
490 |
491 | public void SetClockwiseFaces
492 | (
493 | boolean ClockwiseFaces
494 | )
495 | {
496 | this.ClockwiseFaces = ClockwiseFaces;
497 | requestRender();
498 | } /*SetClockwiseFaces*/
499 |
500 | public boolean GetClockwiseFaces()
501 | {
502 | return
503 | ClockwiseFaces;
504 | } /*GetClockwiseFaces*/
505 |
506 | public void SetOrientation
507 | (
508 | Rotation NewOrientation,
509 | android.view.animation.Interpolator AnimFunction, /* optional */
510 | float AnimDuration /* ignored unless AnimFunction is specified */
511 | )
512 | {
513 | if (AnimFunction != null)
514 | {
515 | new RotationAnimator
516 | (
517 | /*AnimFunction =*/ AnimFunction,
518 | /*Duration =*/ AnimDuration,
519 | /*EndRotation =*/ NewOrientation
520 | );
521 | }
522 | else
523 | {
524 | CurRotation = NewOrientation;
525 | requestRender();
526 | } /*if*/
527 | } /*SetOrientation*/
528 |
529 | public void SetOrientation
530 | (
531 | Rotation NewOrientation,
532 | boolean Animate
533 | )
534 | {
535 | SetOrientation
536 | (
537 | /*NewOrientation =*/ NewOrientation,
538 | /*AnimFunction =*/
539 | Animate ?
540 | new PolyAccelerationDecelerationInterpolator(3.0f)
541 | :
542 | null,
543 | /*AnimDuration =*/ 1.5f
544 | );
545 | } /*SetOrientation*/
546 |
547 | } /*ObjectView*/
548 |
--------------------------------------------------------------------------------
/src/ObjReader.java:
--------------------------------------------------------------------------------
1 | package nz.gen.geek_central.GLUseful;
2 | /*
3 | Basic reader for .obj 3D model files and associated .mtl material files.
4 |
5 | Copyright 2011, 2013 by Lawrence D'Oliveiro .
6 |
7 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 | use this file except in compliance with the License. You may obtain a copy of
9 | the License at
10 |
11 | http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | Unless required by applicable law or agreed to in writing, software
14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 | License for the specific language governing permissions and limitations under
17 | the License.
18 | */
19 |
20 | import java.util.ArrayList;
21 | import static nz.gen.geek_central.GLUseful.GLUseful.gl;
22 |
23 | public class ObjReader
24 | {
25 |
26 | public static class DataFormatException extends RuntimeException
27 | /* indicates a problem parsing file contents. */
28 | {
29 |
30 | public DataFormatException
31 | (
32 | String Message
33 | )
34 | {
35 | super(Message);
36 | } /*DataFormatException*/
37 |
38 | } /*DataFormatException*/
39 |
40 | public static class Material
41 | {
42 | public final String Name;
43 | /* no support for texture files, functions or any of that stuff */
44 | public final GeomBuilder.Color
45 | AmbientColor, DiffuseColor, SpecularColor;
46 | public final float SpecularExponent;
47 | public final int Illum; /* NYI for now? */
48 |
49 | private static final float DefaultSpecularExponent = 100.0f /*?*/;
50 | private static final int DefaultIllum = 0 /*?*/;
51 |
52 | public Material
53 | (
54 | String Name
55 | )
56 | /* material with all default settings. */
57 | {
58 | this.Name = Name;
59 | this.AmbientColor = new GeomBuilder.Color(1.0f, 1.0f, 1.0f, 1.0f);
60 | this.DiffuseColor = new GeomBuilder.Color(1.0f, 1.0f, 1.0f, 1.0f);
61 | this.SpecularColor = new GeomBuilder.Color(1.0f, 1.0f, 1.0f, 1.0f);
62 | this.SpecularExponent = DefaultSpecularExponent;
63 | this.Illum = DefaultIllum;
64 | } /*Material*/
65 |
66 | public Material
67 | (
68 | String Name,
69 | /* all following parameters optional, alpha components ignored, taken from Dissolve */
70 | GeomBuilder.Color AmbientColor,
71 | GeomBuilder.Color DiffuseColor,
72 | GeomBuilder.Color SpecularColor,
73 | Float SpecularExponent,
74 | Float Dissolve,
75 | Integer Illum
76 | )
77 | {
78 | this.Name = Name;
79 | final float Alpha = Dissolve != null ? Dissolve : 1.0f;
80 | this.AmbientColor =
81 | AmbientColor != null ?
82 | new GeomBuilder.Color
83 | (
84 | AmbientColor.r,
85 | AmbientColor.g,
86 | AmbientColor.b,
87 | Alpha
88 | )
89 | :
90 | new GeomBuilder.Color(1.0f, 1.0f, 1.0f, Alpha);
91 | this.DiffuseColor =
92 | DiffuseColor != null ?
93 | new GeomBuilder.Color
94 | (
95 | DiffuseColor.r,
96 | DiffuseColor.g,
97 | DiffuseColor.b,
98 | Alpha
99 | )
100 | :
101 | new GeomBuilder.Color(1.0f, 1.0f, 1.0f, Alpha);
102 | this.SpecularColor =
103 | SpecularColor != null ?
104 | new GeomBuilder.Color
105 | (
106 | SpecularColor.r,
107 | SpecularColor.g,
108 | SpecularColor.b,
109 | Alpha
110 | )
111 | :
112 | new GeomBuilder.Color(1.0f, 1.0f, 1.0f, Alpha);
113 | this.SpecularExponent = SpecularExponent != null ? SpecularExponent : DefaultSpecularExponent;
114 | this.Illum = Illum != null ? Illum : DefaultIllum;
115 | } /*Material*/
116 |
117 | public void Apply()
118 | /* sets the material settings into the current GL context. */
119 | {
120 | /* Illum NYI */
121 | gl.glMaterialfv
122 | (
123 | /*face =*/ gl.GL_FRONT_AND_BACK,
124 | /*pname =*/ gl.GL_AMBIENT,
125 | /*params =*/
126 | new float[]
127 | {
128 | AmbientColor.r,
129 | AmbientColor.g,
130 | AmbientColor.b,
131 | AmbientColor.a,
132 | },
133 | /*offset =*/ 0
134 | );
135 | gl.glMaterialfv
136 | (
137 | /*face =*/ gl.GL_FRONT_AND_BACK,
138 | /*pname =*/ gl.GL_DIFFUSE,
139 | /*params =*/
140 | new float[]
141 | {
142 | DiffuseColor.r,
143 | DiffuseColor.g,
144 | DiffuseColor.b,
145 | DiffuseColor.a,
146 | },
147 | /*offset =*/ 0
148 | );
149 | gl.glMaterialfv
150 | (
151 | /*face =*/ gl.GL_FRONT_AND_BACK,
152 | /*pname =*/ gl.GL_SPECULAR,
153 | /*params =*/
154 | new float[]
155 | {
156 | SpecularColor.r,
157 | SpecularColor.g,
158 | SpecularColor.b,
159 | SpecularColor.a,
160 | },
161 | /*offset =*/ 0
162 | );
163 | gl.glMaterialf
164 | (
165 | /*face =*/ gl.GL_FRONT_AND_BACK,
166 | /*pname =*/ gl.GL_SHININESS,
167 | /*param =*/ SpecularExponent
168 | );
169 | } /*Apply*/
170 |
171 | } /*Material*/
172 |
173 | public static class MaterialSet
174 | /* a mapping from material names to material definitions */
175 | {
176 | private final java.util.Map Materials =
177 | new java.util.HashMap();
178 |
179 | public MaterialSet()
180 | {
181 | } /*MaterialSet*/
182 |
183 | public void Add
184 | (
185 | Material TheMaterial
186 | )
187 | {
188 | Materials.put(TheMaterial.Name, TheMaterial);
189 | } /*Add*/
190 |
191 | public Material Get
192 | (
193 | String Name
194 | )
195 | {
196 | return
197 | Materials.get(Name);
198 | } /*Get*/
199 |
200 | } /*MaterialSet*/
201 |
202 | private static class ObjTokenizer
203 | {
204 | private final java.io.InputStream Input;
205 | public boolean EOL, EOF;
206 | private boolean LastWasCR, InComment;
207 | public int LineNr, ColNr;
208 |
209 | public void Fail
210 | (
211 | String Msg
212 | )
213 | {
214 | throw new DataFormatException
215 | (
216 | String.format
217 | (
218 | GLUseful.StdLocale,
219 | "ObjReader error at line %d, col %d: %s",
220 | LineNr, ColNr,
221 | Msg
222 | )
223 | );
224 | } /*Fail*/
225 |
226 | public ObjTokenizer
227 | (
228 | java.io.InputStream Input
229 | )
230 | {
231 | this.Input = Input;
232 | EOL = false;
233 | EOF = false;
234 | LastWasCR = false;
235 | InComment = false;
236 | LineNr = 1;
237 | ColNr = 0;
238 | } /*ObjTokenizer*/
239 |
240 | private boolean IsSeparator
241 | (
242 | char Ch
243 | )
244 | {
245 | return
246 | Ch <= ' ';
247 | } /*IsSeparator*/
248 |
249 | private boolean IsEOL
250 | (
251 | char Ch
252 | )
253 | {
254 | return
255 | Ch == '\015' || Ch == '\012';
256 | } /*IsEOL*/
257 |
258 | private char NextCh()
259 | {
260 | if (EOF)
261 | {
262 | Fail("read past EOF");
263 | } /*if*/
264 | /*final*/ char Result
265 | = (char)-1; /*sigh*/
266 | try
267 | {
268 | boolean LastWasBackslash = false;
269 | for (;;)
270 | {
271 | ++ColNr;
272 | final int ich = Input.read();
273 | if (ich < 0)
274 | {
275 | --ColNr;
276 | EOF = true;
277 | Result = '\n';
278 | break;
279 | }
280 | else if (LastWasCR && (char)ich == '\012')
281 | {
282 | /* skip LF following CR */
283 | --ColNr;
284 | LastWasCR = false;
285 | LastWasBackslash = false;
286 | }
287 | else if (!LastWasBackslash && (char)ich == '\\')
288 | {
289 | LastWasBackslash = true;
290 | LastWasCR = false;
291 | }
292 | else if (!LastWasBackslash || !IsEOL((char)ich))
293 | {
294 | Result = (char)ich;
295 | break;
296 | }
297 | else
298 | {
299 | ++LineNr;
300 | ColNr = 0;
301 | LastWasCR = (char)ich == '\015';
302 | LastWasBackslash = false;
303 | } /*if*/
304 | } /*for*/
305 | }
306 | catch (java.io.IOException IOError)
307 | {
308 | Fail("input error: " + IOError.toString());
309 | } /*try*/
310 | EOL = EOF || IsEOL(Result);
311 | LastWasCR = Result == '\015';
312 | return
313 | Result;
314 | } /*NextCh*/
315 |
316 | public String NextSym
317 | (
318 | boolean Required
319 | )
320 | /* fetches next symbol from current line, or null if none. */
321 | {
322 | final StringBuilder CurSym = new StringBuilder();
323 | for (;;)
324 | {
325 | if (EOL)
326 | break;
327 | final char Ch = NextCh();
328 | if (!InComment)
329 | {
330 | if (IsSeparator(Ch))
331 | {
332 | if (CurSym.length() != 0)
333 | break;
334 | }
335 | else if (Ch == '#')
336 | {
337 | InComment = true;
338 | }
339 | else
340 | {
341 | CurSym.appendCodePoint((int)Ch);
342 | } /*if*/
343 | } /*if*/
344 | } /*for*/
345 | final String Result =
346 | CurSym.length() != 0 ?
347 | CurSym.toString()
348 | :
349 | null;
350 | if (Result == null && Required)
351 | {
352 | Fail("missing required symbol");
353 | } /*if*/
354 | return
355 | Result;
356 | } /*NextSym*/
357 |
358 | public void EndLine()
359 | /* skips rest of current line (must be nothing more than whitespace left). */
360 | {
361 | for (;;)
362 | {
363 | if (EOL)
364 | break;
365 | final char Ch = NextCh();
366 | if (IsEOL(Ch))
367 | break;
368 | if (!InComment && !IsSeparator(Ch))
369 | {
370 | Fail("unexpected stuff at end of line");
371 | } /*if*/
372 | } /*for*/
373 | EOL = false;
374 | if (!EOF)
375 | {
376 | ++LineNr;
377 | ColNr = 0;
378 | } /*if*/
379 | InComment = false;
380 | } /*EndLine*/
381 |
382 | public String GetRestOfLine()
383 | /* returns rest of current line (excluding leading whitespace, but
384 | including embedded whitespace) as a symbol. There must be at least
385 | one non-whitespace character. */
386 | {
387 | final StringBuilder CurSym = new StringBuilder();
388 | for (;;)
389 | {
390 | if (EOL)
391 | break;
392 | final char Ch = NextCh();
393 | if (!InComment)
394 | {
395 | if (IsEOL(Ch))
396 | {
397 | break;
398 | }
399 | else if (IsSeparator(Ch) && CurSym.length() == 0)
400 | {
401 | /* ignore leading whitespace */
402 | }
403 | else if (Ch == '#')
404 | {
405 | InComment = true;
406 | }
407 | else
408 | {
409 | CurSym.appendCodePoint((int)Ch);
410 | } /*if*/
411 | } /*if*/
412 | } /*for*/
413 | final String Result =
414 | CurSym.length() != 0 ?
415 | CurSym.toString()
416 | :
417 | null;
418 | if (Result == null)
419 | {
420 | Fail("missing required symbol");
421 | } /*if*/
422 | return
423 | Result;
424 | } /*GetRestOfLine*/
425 |
426 | public float GetFloat
427 | (
428 | String Description
429 | )
430 | {
431 | float Result = 0.0f;
432 | try
433 | {
434 | Result = Float.parseFloat(NextSym(true));
435 | }
436 | catch (NumberFormatException BadNum)
437 | {
438 | Fail("bad " + Description);
439 | } /*try*/
440 | return
441 | Result;
442 | } /*GetFloat*/
443 |
444 | public Vec3f GetVec
445 | (
446 | int MinD, /* [1 .. 3] */
447 | int MaxD /* [3 .. 4] */
448 | )
449 | {
450 | final String XStr = NextSym(true);
451 | final String YStr = NextSym(MinD >= 2);
452 | final String ZStr = NextSym(MinD >= 3);
453 | final String WStr =
454 | MaxD == 4 ?
455 | NextSym(MinD == 4)
456 | :
457 | null;
458 | /*final*/ Vec3f Result
459 | = null; /*sigh*/
460 | try
461 | {
462 | final float Y =
463 | YStr != null ?
464 | Float.parseFloat(YStr)
465 | :
466 | 1.0f;
467 | final float Z =
468 | ZStr != null ?
469 | Float.parseFloat(ZStr)
470 | :
471 | 1.0f;
472 | final float W =
473 | WStr != null ?
474 | Float.parseFloat(WStr)
475 | :
476 | 1.0f;
477 | Result = new Vec3f
478 | (
479 | Float.parseFloat(XStr) / W,
480 | Y / W,
481 | Z / W
482 | );
483 | }
484 | catch (NumberFormatException BadNum)
485 | {
486 | Fail("bad vector coordinate");
487 | } /*try*/
488 | return
489 | Result;
490 | } /*GetVec*/
491 |
492 | public GeomBuilder.Color GetColor()
493 | {
494 | /* colours in .obj don't have individual alpha */
495 | final String RStr = NextSym(true);
496 | final String GStr = NextSym(true);
497 | final String BStr = NextSym(true);
498 | /*final*/ GeomBuilder.Color Result
499 | = null; /*sigh*/
500 | try
501 | {
502 | Result = new GeomBuilder.Color
503 | (
504 | Float.parseFloat(RStr),
505 | Float.parseFloat(GStr),
506 | Float.parseFloat(BStr),
507 | 1.0f
508 | );
509 | }
510 | catch (NumberFormatException BadNum)
511 | {
512 | Fail("bad RGB colour component");
513 | } /*try*/
514 | return
515 | Result;
516 | } /*GetColor*/
517 |
518 | public void SkipRest()
519 | {
520 | while (!EOL)
521 | {
522 | NextCh();
523 | } /*while*/
524 | } /*SkipRest*/
525 |
526 | } /*ObjTokenizer*/
527 |
528 | private static class FaceVert
529 | {
530 | public final Integer VertIndex, TexCoordIndex, NormalIndex;
531 |
532 | public FaceVert
533 | (
534 | Integer VertIndex, /* mandatory */
535 | Integer TexCoordIndex, /* optional */
536 | Integer NormalIndex /* optional */
537 | )
538 | {
539 | this.VertIndex = VertIndex;
540 | this.TexCoordIndex = TexCoordIndex;
541 | this.NormalIndex = NormalIndex;
542 | } /*FaceVert*/
543 |
544 | } /*FaceVert*/
545 |
546 | public interface MaterialLoader
547 | {
548 |
549 | public MaterialSet Load
550 | (
551 | MaterialSet PrevMaterials, /* can simply add to this or replace it */
552 | String FileName
553 | );
554 |
555 | } /*MaterialLoader*/
556 |
557 | public static class Model
558 | {
559 | public final Vec3f BoundMin, BoundMax;
560 |
561 | private static class Component
562 | {
563 | private final Material ObjMaterial; /* optional */
564 | private final GeomBuilder.Obj ObjGeometry;
565 |
566 | private Component
567 | (
568 | Material ObjMaterial,
569 | GeomBuilder.Obj ObjGeometry
570 | )
571 | {
572 | this.ObjMaterial = ObjMaterial;
573 | this.ObjGeometry = ObjGeometry;
574 | } /*Component*/
575 | } /*Component*/
576 |
577 | private final Component[] Components;
578 |
579 | private Model
580 | (
581 | Component[] Components,
582 | Vec3f BoundMin,
583 | Vec3f BoundMax
584 | )
585 | {
586 | this.Components = Components;
587 | this.BoundMin = BoundMin;
588 | this.BoundMax = BoundMax;
589 | } /*Model*/
590 |
591 | public void Draw()
592 | {
593 | for (Component ThisComponent : Components)
594 | {
595 | if (ThisComponent.ObjMaterial != null)
596 | {
597 | ThisComponent.ObjMaterial.Apply();
598 | } /*if*/
599 | ThisComponent.ObjGeometry.Draw();
600 | } /*for*/
601 | } /*Draw*/
602 |
603 | } /*Model*/
604 |
605 | public static Model ReadObj
606 | (
607 | java.io.InputStream From,
608 | MaterialLoader LoadMaterials
609 | )
610 | throws DataFormatException
611 | {
612 | final ObjTokenizer Parse = new ObjTokenizer(From);
613 | ArrayList
614 | Vertices = null,
615 | TexCoords = null,
616 | Normals = null;
617 | ArrayList Faces = null;
618 | MaterialSet LoadedMaterials = new MaterialSet();
619 | final ArrayList ModelComponents = new ArrayList();
620 | Material CurMaterial = new Material("");
621 | Vec3f
622 | BoundMin = null,
623 | BoundMax = null;
624 | for (;;)
625 | {
626 | String Op = Parse.NextSym(false);
627 | if (Op != null)
628 | {
629 | Op = Op.intern();
630 | } /*if*/
631 | if (Op == null && Parse.EOF || Op == "usemtl")
632 | {
633 | if (Faces != null)
634 | {
635 | /* finish last object */
636 | final GeomBuilder Geom = new GeomBuilder
637 | (
638 | /*GotNormals =*/ Normals != null,
639 | /*GotTexCoords =*/ TexCoords != null,
640 | /*GotColors =*/ false
641 | );
642 | final java.util.Map FaceMap =
643 | new java.util.HashMap();
644 | for (FaceVert[] Face : Faces)
645 | {
646 | for (FaceVert Vert : Face)
647 | {
648 | if (!FaceMap.containsKey(Vert))
649 | {
650 | FaceMap.put
651 | (
652 | Vert,
653 | Geom.Add
654 | (
655 | /*Vertex =*/ Vertices.get(Vert.VertIndex),
656 | /*Normal =*/
657 | Vert.NormalIndex != null ?
658 | Normals.get(Vert.NormalIndex)
659 | :
660 | null,
661 | /*TexCoord =*/
662 | Vert.TexCoordIndex != null ?
663 | TexCoords.get(Vert.TexCoordIndex)
664 | :
665 | null,
666 | /*VertexColor =*/ null
667 | )
668 | );
669 | } /*if*/
670 | } /*for*/
671 | final int[] FaceVerts = new int[Face.length];
672 | for (int i = 0; i < Face.length; ++i)
673 | {
674 | FaceVerts[i] = FaceMap.get(Face[i]);
675 | } /*for*/
676 | Geom.AddPoly(FaceVerts);
677 | } /*for*/
678 | final GeomBuilder.Obj NewObj = Geom.MakeObj();
679 | if (BoundMin != null)
680 | {
681 | BoundMin =
682 | new Vec3f
683 | (
684 | Math.min(BoundMin.x, NewObj.BoundMin.x),
685 | Math.min(BoundMin.y, NewObj.BoundMin.y),
686 | Math.min(BoundMin.z, NewObj.BoundMin.z)
687 | );
688 | }
689 | else
690 | {
691 | BoundMin = NewObj.BoundMin;
692 | } /*if*/
693 | if (BoundMax != null)
694 | {
695 | BoundMax =
696 | new Vec3f
697 | (
698 | Math.max(BoundMax.x, NewObj.BoundMax.x),
699 | Math.max(BoundMax.y, NewObj.BoundMax.y),
700 | Math.max(BoundMax.z, NewObj.BoundMax.z)
701 | );
702 | }
703 | else
704 | {
705 | BoundMax = NewObj.BoundMax;
706 | } /*if*/
707 | ModelComponents.add
708 | (
709 | new Model.Component(CurMaterial, NewObj)
710 | );
711 | Faces = null;
712 | } /*if*/
713 | if (Op != null)
714 | {
715 | CurMaterial = LoadedMaterials.Get(Parse.NextSym(true));
716 | } /*if*/
717 | }
718 | else if (Op != null)
719 | {
720 | if (Op == "v")
721 | {
722 | final Vec3f Vec = Parse.GetVec(3, 4);
723 | if (Vertices == null)
724 | {
725 | Vertices = new ArrayList();
726 | } /*if*/
727 | Vertices.add(Vec);
728 | }
729 | else if (Op == "vt")
730 | {
731 | final Vec3f Vec = Parse.GetVec(1, 3);
732 | if (TexCoords == null)
733 | {
734 | TexCoords = new ArrayList();
735 | } /*if*/
736 | TexCoords.add(Vec);
737 | }
738 | else if (Op == "vn")
739 | {
740 | final Vec3f Vec = Parse.GetVec(3, 3);
741 | if (Normals == null)
742 | {
743 | Normals = new ArrayList();
744 | } /*if*/
745 | Normals.add(Vec);
746 | }
747 | else if (Op == "f")
748 | {
749 | if (Faces == null)
750 | {
751 | Faces = new ArrayList();
752 | } /*if*/
753 | final ArrayList FaceVerts = new ArrayList();
754 | for (;;)
755 | {
756 | final String VertStr = Parse.NextSym(false);
757 | if (VertStr == null)
758 | break;
759 | Integer VertIndex = null, TexCoordIndex = null, NormalIndex = null;
760 | int Which = 0, ThisPos = 0, LastPos;
761 | for (;;)
762 | {
763 | LastPos = ThisPos;
764 | for (;;)
765 | {
766 | if (ThisPos == VertStr.length())
767 | break;
768 | if (VertStr.charAt(ThisPos) == '/')
769 | break;
770 | ++ThisPos;
771 | } /*for*/
772 | if (ThisPos > LastPos)
773 | {
774 | int ThisComponent
775 | = -1; /*sigh*/
776 | try
777 | {
778 | ThisComponent = Integer.parseInt
779 | (
780 | VertStr.substring(LastPos, ThisPos)
781 | );
782 | }
783 | catch (NumberFormatException BadNum)
784 | {
785 | Parse.Fail(String.format(GLUseful.StdLocale, "bad vertex index \"%s\"", VertStr.substring(LastPos, ThisPos)));
786 | } /*try*/
787 | switch (Which)
788 | {
789 | case 0:
790 | if (Vertices == null)
791 | {
792 | Parse.Fail("vertices referenced but not defined");
793 | } /*if*/
794 | if (ThisComponent < 0)
795 | {
796 | ThisComponent += Vertices.size();
797 | }
798 | else
799 | {
800 | ThisComponent -= 1;
801 | } /*if*/
802 | if (ThisComponent < 0 || ThisComponent >= Vertices.size())
803 | {
804 | Parse.Fail("vertex reference out of range");
805 | } /*if*/
806 | VertIndex = ThisComponent;
807 | break;
808 | case 1:
809 | if (TexCoords == null)
810 | {
811 | Parse.Fail("texcoords referenced but not defined");
812 | } /*if*/
813 | if (ThisComponent < 0)
814 | {
815 | ThisComponent += TexCoords.size();
816 | }
817 | else
818 | {
819 | ThisComponent -= 1;
820 | } /*if*/
821 | if (ThisComponent < 0 || ThisComponent >= TexCoords.size())
822 | {
823 | Parse.Fail("texcoord reference out of range");
824 | } /*if*/
825 | TexCoordIndex = ThisComponent;
826 | break;
827 | case 2:
828 | if (Normals == null)
829 | {
830 | Parse.Fail("normals referenced but not defined");
831 | } /*if*/
832 | if (ThisComponent < 0)
833 | {
834 | ThisComponent += Normals.size();
835 | }
836 | else
837 | {
838 | ThisComponent -= 1;
839 | } /*if*/
840 | if (ThisComponent < 0 || ThisComponent >= Normals.size())
841 | {
842 | Parse.Fail("normal reference out of range");
843 | } /*if*/
844 | NormalIndex = ThisComponent;
845 | break;
846 | } /*switch*/
847 | } /*if*/
848 | ++Which;
849 | if (ThisPos == VertStr.length())
850 | break;
851 | if (Which == 3)
852 | {
853 | Parse.Fail("too many components for face vertex");
854 | } /*if*/
855 | ++ThisPos; /* skip slash */
856 | } /*for*/
857 | if (VertIndex == null)
858 | {
859 | Parse.Fail("missing vertex reference");
860 | } /*if*/
861 | if (TexCoordIndex == null && TexCoords != null)
862 | {
863 | Parse.Fail("missing texcoord reference");
864 | } /*if*/
865 | if (NormalIndex == null && Normals != null)
866 | {
867 | Parse.Fail("missing normal reference");
868 | } /*if*/
869 | FaceVerts.add(new FaceVert(VertIndex, TexCoordIndex, NormalIndex));
870 | } /*for*/
871 | Faces.add(FaceVerts.toArray(new FaceVert[FaceVerts.size()]));
872 | }
873 | else if (Op == "mtllib")
874 | {
875 | /* Blender only expects a single library file name, which might have embedded spaces */
876 | LoadedMaterials = LoadMaterials.Load(LoadedMaterials, Parse.GetRestOfLine());
877 | if (LoadedMaterials == null)
878 | {
879 | Parse.Fail
880 | (
881 | "materials loader didn't return anything"
882 | );
883 | } /*if*/
884 | }
885 | else
886 | {
887 | System.err.printf
888 | (
889 | "ObjReader.ReadObj warning: ignoring op \"%s\" on line %d.\n",
890 | Op,
891 | Parse.LineNr
892 | );
893 | Parse.SkipRest();
894 | } /*if*/
895 | } /*if*/
896 | Parse.EndLine();
897 | if (Parse.EOF && Faces == null)
898 | break;
899 | } /*for*/
900 | return
901 | new Model
902 | (
903 | ModelComponents.toArray(new Model.Component[ModelComponents.size()]),
904 | BoundMin,
905 | BoundMax
906 | );
907 | } /*ReadObj*/
908 |
909 | public static MaterialSet ReadMaterials
910 | (
911 | java.io.InputStream From,
912 | MaterialSet CurMaterials
913 | )
914 | {
915 | final ObjTokenizer Parse = new ObjTokenizer(From);
916 | String MaterialName = null;
917 | GeomBuilder.Color
918 | AmbientColor = null,
919 | DiffuseColor = null,
920 | SpecularColor = null;
921 | Float
922 | SpecularExponent = null,
923 | Dissolve = null;
924 | Integer
925 | Illum = null;
926 | for (;;)
927 | {
928 | String Op = Parse.NextSym(false);
929 | if (Op != null)
930 | {
931 | Op = Op.intern();
932 | } /*if*/
933 | if (Op == null && Parse.EOF || Op == "newmtl")
934 | {
935 | if (MaterialName != null)
936 | {
937 | if (!CurMaterials.Materials.containsKey(MaterialName)) /* let earlier entry take precedence */
938 | {
939 | CurMaterials.Materials.put
940 | (
941 | MaterialName,
942 | new Material
943 | (
944 | /*Name =*/ MaterialName,
945 | /*AmbientColor =*/ AmbientColor,
946 | /*DiffuseColor =*/ DiffuseColor,
947 | /*SpecularColor =*/ SpecularColor,
948 | /*SpecularExponent =*/ SpecularExponent,
949 | /*Dissolve =*/ Dissolve,
950 | /*Illum =*/ Illum
951 | )
952 | );
953 | } /*if*/
954 | MaterialName = null;
955 | } /*if*/
956 | if (Op != null)
957 | {
958 | MaterialName = Parse.NextSym(true).intern();
959 | AmbientColor = null;
960 | DiffuseColor = null;
961 | SpecularColor = null;
962 | SpecularExponent = null;
963 | Dissolve = null;
964 | Illum = null;
965 | } /*if*/
966 | }
967 | else if (Op != null)
968 | {
969 | if (MaterialName == null)
970 | {
971 | Parse.Fail("material definition doesn’t begin with “newmtl”");
972 | } /*if*/
973 | if (Op == "Ka")
974 | {
975 | AmbientColor = Parse.GetColor();
976 | }
977 | else if (Op == "Kd")
978 | {
979 | DiffuseColor = Parse.GetColor();
980 | }
981 | else if (Op == "Ks")
982 | {
983 | SpecularColor = Parse.GetColor();
984 | }
985 | else if (Op == "Ns")
986 | {
987 | SpecularExponent = Parse.GetFloat("specular exponent");
988 | }
989 | else if (Op == "d")
990 | {
991 | Dissolve = Parse.GetFloat("dissolve");
992 | }
993 | /* "illum" NYI */
994 | else
995 | {
996 | System.err.printf
997 | (
998 | "ObjReader.ReadMaterials warning: ignoring op \"%s\" on line %d.\n",
999 | Op,
1000 | Parse.LineNr
1001 | );
1002 | Parse.SkipRest();
1003 | } /*if*/
1004 | } /*if*/
1005 | Parse.EndLine();
1006 | if (Parse.EOF && MaterialName == null)
1007 | break;
1008 | } /*for*/
1009 | return
1010 | CurMaterials;
1011 | } /*ReadMaterials*/
1012 |
1013 | public static Model ReadObj
1014 | (
1015 | String FileName,
1016 | MaterialLoader LoadMaterials
1017 | )
1018 | throws DataFormatException
1019 | {
1020 | Model Result = null;
1021 | java.io.FileInputStream ReadFrom = null;
1022 | try
1023 | {
1024 | ReadFrom = new java.io.FileInputStream(FileName);
1025 | }
1026 | catch (java.io.IOException IOError)
1027 | {
1028 | throw new DataFormatException("I/O error: " + IOError.toString());
1029 | } /*try*/
1030 | if (ReadFrom != null)
1031 | {
1032 | Result = ReadObj(ReadFrom, LoadMaterials);
1033 | try
1034 | {
1035 | ReadFrom.close();
1036 | }
1037 | catch (java.io.IOException WhoCares)
1038 | {
1039 | /* I mean, really? */
1040 | } /*try*/
1041 | } /*if*/
1042 | return
1043 | Result;
1044 | } /*ReadObj*/
1045 |
1046 | public static MaterialSet ReadMaterials
1047 | (
1048 | String FileName,
1049 | MaterialSet CurMaterials
1050 | )
1051 | {
1052 | MaterialSet Result = null;
1053 | java.io.FileInputStream ReadFrom = null;
1054 | try
1055 | {
1056 | ReadFrom = new java.io.FileInputStream(FileName);
1057 | }
1058 | catch (java.io.IOException IOError)
1059 | {
1060 | throw new DataFormatException("I/O error: " + IOError.toString());
1061 | } /*try*/
1062 | if (ReadFrom != null)
1063 | {
1064 | Result = ReadMaterials(ReadFrom, CurMaterials);
1065 | try
1066 | {
1067 | ReadFrom.close();
1068 | }
1069 | catch (java.io.IOException WhoCares)
1070 | {
1071 | /* I mean, really? */
1072 | } /*try*/
1073 | } /*if*/
1074 | return
1075 | Result;
1076 | } /*ReadMaterials*/
1077 |
1078 | } /*ObjReader*/
1079 |
--------------------------------------------------------------------------------