Androidで3Dを扱いたい!
世はまさにみんな3DモデルをBlenderで作ってUnityやUnreal Engineに組み込む時代。
しかし、私が前に使っていたパソコンではなんとBlenderが動かなかったのだ!
当時の私は、どうにかして3Dゲームを作れないかと夜な夜な検索をした末にOpenGLへ飛びついた。
だが、OpenGL×Android×Javaの開発ブログは、調べても非常に少ない。
さらに、私が飛びついたのはまさかのOpenGL ES1.0。
OpenGL ES2.0以降が推奨される時代に、である。
私の作業は難航した。
その末にたどり着いたOpenGLを用いた3D表現の方法を共有したいと思います。
この記事で行うこと
AndroidアプリケーションをJavaで作り、OpenGL ES1.0を用いて頂点データとテクスチャ用の画像を用いて3Dモデル(もどき)の立方体を表示します。独自流であるため、簡単に立ち上げて使ってみようという人にはあまり向かないと思います。
制作環境
OS
・Ubuntu 22.04.5 LTS
Android Studio
・Otter | 2025.2.1
・Ladybug | 2024.2.1
実況動作確認環境のスマホのAndroidバージョン
・Android 11
・Android 13
1.取り敢えず新規プロジェクトを作りましょう
Android Studioを起動し、
Welcome to Android Studioの画面から
Projects>New Projectを選択します。
Empty Views Activityを選択します。
NameとSave locationを入力し、LanguageはJavaにしてください。Save locationのパスのusernameにはあなたのユーザー名が入ります。APIは適当に。
ちょっと待ちます

このように新規プロジェクトが立ち上がります。画面上部に「Gradle project sync in progress...」と表示される場合は、表示が消えるまで待ちます。
2.次に、MainActivityとactivity_main.xmlを書き込みます
MainActivity.javaを下記内容に置換してください。
以下、activity_main.xmlの内容も同様に置換してください。
MainActivity
MainActivity (java/com/example/test/MainActivity.java)
package com.example.test;
import android.os.Bundle;
import android.widget.LinearLayout;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
public static MODEL3D m = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
set_GLView();
m = reading_file_system.read_model(this.getApplicationContext(),"box");
}
void set_GLView() {
setContentView(R.layout.activity_main);
LinearLayout gamecanvas = findViewById(R.id.glview);
gamecanvas.removeAllViews();
gamecanvas.addView(new GLView(getApplicationContext()));
}
}
activity_main.xml (レイアウトファイル)
activity_main.xml (res/layout/activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/glview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivityを打ち込むと、この時点ではMODEL3Dとreading_file_systemが赤字になります。当然です。あとあと作ります。
3.OpenGLを表示するViewを作ります
GLView
GLSurfaceViewを継承したカスタムViewのクラスを作ります。
com.example.testの上で右クリック>New>Java Class>Classを選択した状態で、Nameに「GLView」と入力してEnter。するとGLView.javaが作成されるので、下のスクリプトに置き換えてください。
GLView (java/com/example/test/GLView.java)
package com.example.test;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import static javax.microedition.khronos.opengles.GL10.GL_COLOR_BUFFER_BIT;
public class GLView extends GLSurfaceView {
public GLView(Context context) {
super(context);
this.getHolder().setFormat(PixelFormat.RGBA_8888);
Renderer mRenderer = new LocalRenderer();
setRenderer(mRenderer);
this.setBackgroundColor(Color.argb(0,0,0,0));
this.setRenderMode(RENDERMODE_CONTINUOUSLY);
}
public static class LocalRenderer implements Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glClear(GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glClearColor(0, 0, 0, 0);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0, 0, width, height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, 100.0f, (float)width / height, 0.01f, 2500f);
}
@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
gl.glEnable(GL10.GL_COLOR_MATERIAL);
//ちょっと視点をななめ上から見下ろす感じに
gl.glTranslatef(0,-1,-4);
gl.glRotatef(30, 1, 0, 0);
gl.glRotatef(45, 0, 1, 0);
MainActivity.m.draw(gl,new float[]{0f,0f,0f},new float[]{0f,0f,0f});
}
}
}
上のスクリプトをコピペすると、MainActivity.m.draw(gl,new float[]{0f,0f,0f},new float[]{0f,0f,0f});のdrawが赤字になります。これから作っていきましょう。
4.モデルを扱うViewを作ります
以下、「GLView」の作成と同様に「MODEL3D」を作成します。
MODEL3D
MODEL3D (java/com/example/test/MODEL3D.java)
package com.example.test;
import static android.opengl.GLES10.glBlendFunc;
import static android.opengl.GLES10.glDisable;
import static javax.microedition.khronos.opengles.GL10.GL_BLEND;
import static javax.microedition.khronos.opengles.GL10.GL_CLAMP_TO_EDGE;
import static javax.microedition.khronos.opengles.GL10.GL_ONE_MINUS_SRC_ALPHA;
import static javax.microedition.khronos.opengles.GL10.GL_SRC_ALPHA;
import static javax.microedition.khronos.opengles.GL10.GL_TEXTURE_2D;
import static javax.microedition.khronos.opengles.GL10.GL_TEXTURE_WRAP_S;
import static javax.microedition.khronos.opengles.GL10.GL_TEXTURE_WRAP_T;
import android.graphics.Bitmap;
import android.opengl.GLUtils;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;
public class MODEL3D {
private final FloatBuffer mmVertexBuffer;
private final FloatBuffer mmTextureBuffer;
private final Bitmap[] mmBitmap_textures;
private final int[] mids;
private int[] mTextures = null;
public MODEL3D(@NonNull float[] vertices, @NonNull Bitmap[] textures,@NonNull int[] ids) {
this.mmVertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
this.mmVertexBuffer.put(vertices);
this.mmVertexBuffer.position(0);
float[] texture = new float[vertices.length*2/3];
for(int y = 0;y<vertices.length/12;y++){
texture[y*8] = 0.0f;
texture[y*8+1] = 1.0f;
texture[y*8+2] = 1.0f;
texture[y*8+3] = 1.0f;
texture[y*8+4] = 1.0f;
texture[y*8+5] = 0.0f;
texture[y*8+6] = 0.0f;
texture[y*8+7] = 0.0f;
}
this.mmTextureBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
this.mmTextureBuffer.put(texture);
this.mmTextureBuffer.position(0);
this.mmBitmap_textures = textures;
this.mids = ids;
}
public void draw(@NonNull GL10 gl,@NonNull float[] Dxyz, @NonNull float[] Rxyz) {
this.init(gl,this.mmBitmap_textures);
gl.glEnable(GL10.GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
gl.glEnable(GL_BLEND);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, mmVertexBuffer);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, mmTextureBuffer);
gl.glTranslatef(Dxyz[0],Dxyz[1],Dxyz[2]);
gl.glRotatef(Rxyz[1],0,1,0);
gl.glRotatef(Rxyz[2],0,0,1);
gl.glRotatef(Rxyz[0],1,0,0);
gl.glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
for (int x = 0;x<this.mids.length;x++) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, this.mTextures[this.mids[x]]);
gl.glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
gl.glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, x*4, 4);
}
gl.glRotatef(-Rxyz[0],1,0,0);
gl.glRotatef(-Rxyz[2],0,0,1);
gl.glRotatef(-Rxyz[1],0,1,0);
gl.glTranslatef(-Dxyz[0],-Dxyz[1],-Dxyz[2]);
}
public void init(@NonNull GL10 gl,Bitmap[] textures) {
if(this.mTextures == null) {
this.mTextures = new int[textures.length];
gl.glGenTextures(textures.length, this.mTextures, 0);
gl.glActiveTexture(GL10.GL_TEXTURE0);
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE);
gl.glEnable(GL10.GL_ALPHA_TEST);
gl.glAlphaFunc(GL10.GL_GEQUAL, 0.1f);
for (int i = 0; i < textures.length; i++) {
gl.glBindTexture(GL10.GL_TEXTURE_2D, this.mTextures[i]);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, textures[i], 0);
}
glDisable(GL_BLEND);
}
}
}
こちらが3Dモデルを描画するためのクラスです。
ざっと引数について述べると、
float[] vertices
頂点データです。
new float[]{
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
}
のようにして、12個の連続した数値([X,Y,Z][X,Y,Z][X,Y,Z][X,Y,Z])で面を1つ表します。
上の例でいうと、
[-0.5f, -0.5f, 0.5f]
[0.5f, -0.5f, 0.5f]
[0.5f, 0.5f, 0.5f]
[-0.5f, 0.5f, 0.5f]
の4点を通る面を作って という意味になります。
Bitmap[] textures
Bitmap型で用意されたテクスチャとして貼り付けるための画像が入ります。
int[] ids
nをインデックス番号としてids[n]はvertices[n×12]〜vertices[n×12+11]で表される面に貼り付けるtexturesのインデックスを表します。
例えば、textures[2]に格納された画像をvertices[0]〜vertices[11]で表される面に貼り付けたい場合、new int[]{2}と表し、さらに追加でtextures[1]に格納された画像をvertices[12]〜vertices[23]で表される面とvertices[24]〜vertices[35]で表される面にそれぞれ貼り付けたい場合、new int[]{2,1}と表します。
5.モデルをAssetsから読み取るクラスを作りましょう
「GLView」「MODEL3D」の作成と同様に「reading_file_system」を作成します。
6.で配置する.txtファイルから文字を読み込むクラスになります。
reading_file_system
reading_file_system (java/com/example/test/reading_file_system.java)
package com.example.test;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class reading_file_system{
public static MODEL3D read_model(Context context,String object){
float[] vertices = conversion_string_to_float(read_text(context,object+"/vertices.txt").split(","));
String[] path = read_text(context,object+"/path.txt").split(",");
int[] id = conversion_string_to_int(read_text(context,object+"/id.txt").split(","));
Bitmap[] bitmap = new Bitmap[path.length];
for(int i = 0;i < path.length;i++){
bitmap[i] = read_image(context,object+"/texture/"+path[i]);
}
return new MODEL3D(vertices,bitmap,id);
}
private static String read_text(Context context,String path){
InputStream is = null;
BufferedReader br = null;
String text = "";
try {
try {
is = context.getAssets().open(path);
br = new BufferedReader(new InputStreamReader(is));
String str;
while ((str = br.readLine()) != null) {
text = text.concat(str + ",");
}
} finally {
if (is != null) is.close();
if (br != null) br.close();
}
} catch (Exception e){
// エラー発生時の処理
}
text = text.replace("\n","").replace(" ","").replace(",,",",").replace(",,",",");
return text;
}
private static Bitmap read_image(Context context,String path){
AssetManager assetManager = context.getAssets();
InputStream istr;
Bitmap bitmap = null;
try {
istr = assetManager.open(path);
bitmap = BitmapFactory.decodeStream(istr);
} catch (IOException e) {
// エラー発生時の処理
}
return bitmap;
}
private static float[] conversion_string_to_float(String[] strings){
float[] floats = new float[strings.length];
for(int i = 0;i < floats.length;i++){
floats[i] = Float.parseFloat(strings[i]);
}
return floats;
}
private static int[] conversion_string_to_int(String[] strings){
int[] ints = new int[strings.length];
for(int i = 0;i < ints.length;i++){
ints[i] = Integer.parseInt(strings[i]);
}
return ints;
}
}
6.Assetsフォルダ内に「3Dモデルのファイル(もどき)」を作りましょう
3Dモデルに興味を持ったことがある方なら、
.glb
.vrm
.fbx
.obj
あたりの拡張子を耳にしたことがあるかと思います。
しかし、これらのファイルは手軽に作れる反面、サイズが大きくなりがちです。
そこで!自分で3Dモデルのファイル(もどき)を作ってしまおうというわけです。
文字と画像だけならとっても軽い!
まずAssetsフォルダを追加します
(Folder LocationはそのままでOK>Finish)
すると、/home/username/AndroidStudioProjects/Test/app/src/main/にassetsと書かれたフォルダが誕生しているので、このなかに階層構造を作っていきましょう。
3Dモデルのファイル(もどき)
- box(モデル名)
- vertices.txt
- path.txt
- id.txt
- texture
- texture_001.png
このような構造にしました。ファイルマネージャーで開くと、下の画像のようになっています。
assetsフォルダの中にboxフォルダを作成し、vertices.txt、path.txt、id.txtの3つのファイルを作成し、さらにboxフォルダの中にtextureフォルダを作成して、中にtexture_001.pngを配置しています。
今回は手軽な立方体を表示させてみたいと思います。
3つの.txtファイルの中身
vertices.txt (頂点データ)
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, 0.5f,
-0.5f, 0.5f, 0.5f,
0.5f, 0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, -0.5f, -0.5f,
-0.5f, -0.5f, -0.5f
path.txt (テクスチャのパス)
texture_001.png
id.txt (面-テクスチャのパスの対応)
0
0
0
0
0
0
texture_001.pngには立方体に貼りたい画像を用意して、配置してください。
↑私は白枠で中身が黒色の64×64のテクスチャ画像を置きました。16の倍数の正方形の画像を推奨します。
用意ができたら、実行してみましょう。
7.表示させてみよう
真ん中上の三角ボタンをクリックして、ビルドします。
スマホの実機で動作させると、
無事に表示されました!
本編、終わり
GLViewの最後の方のスクリプト、
//ちょっと視点を上から見下ろす感じに
gl.glTranslatef(0,-1,-4);
gl.glRotatef(30, 1, 0, 0);
gl.glRotatef(45, 0, 1, 0);
MainActivity.m.draw(gl,new float[]{0f,0f,0f},new float[]{0f,0f,0f});
の数値部分をいじると見え方が変わるのでぜひ楽しんでみてください。
感想
Javaのスクリプトに頂点データや画像のファイルパスを書き込む方式であれば、もっと簡潔に記述できたかもしれません。しかし、私自身のスクリプトが煩雑だったこともあり、扱うモデルが増えるとともに開発が思うように進まなかったゲームもありました。そこで、これならモデルの操作が扱いやすい、と今回紹介したような、独自の方法で3Dモデルの階層構造を設計・配置するスタイルを考案しました。
OpenGLを用いたAndroid開発に関する記事は、WebGLを使ったJavaScript開発の記事と比べても、まだまだ少ないのが現状です。私自身、今でもOpenGLによる開発には苦戦しているほどです。OpenGL ES1.0はすでに非推奨となっていますが、それでも使いたいと考える方にとって、この記事の一部でも参考になれば幸いです。
最後まで読んでいただき、ありがとうございました。
参考文献
OpenGL ES | Views | Android Developers
OpenGL を利用して立方体を描こう! - OpenGL による 3D グラフィックス - Android 開発入門
Android で OpenGL ES を使用する - labs.beatcraft.com










