読者です 読者をやめる 読者になる 読者になる

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Android NDK で OpenGL ES 2.0 x libpng でテクスチャを描画

C++ NDK Android

はじめに

前回三角形を出したので次は画像を読み込んでテクスチャを描画します。
NDK で画像テクスチャを利用する方法は主に 2 通りが挙げられると思います。

  • Java 側の Bitmap クラスを利用する方法
  • C/C++ で画像読み込みライブラリ等を利用する方法

今回は後者について、libpng を利用して PNG をテクスチャとして利用してみたのでメモします。

環境

一式

ファイルは一式は下記からダウンロード出来ます:
gles2ndktest.tar.gz 直

libpng のダウンロード

libpng を利用する方法は 2 通りで、libpng-android 内で ndk-build して static library (libpng.a) を作り png.h とともにプロジェクトへコピーする方法と、プロジェクトへ直接組み込む方法があります。
今回は後者のプロジェクトに組み込む方法でやってみます。

$ git clone git://github.com/julienr/libpng-android.git
$ cp -r libpng-android/jni (自分のプロジェクトのjni)/libpng
$ cd (自分のプロジェクトのjni)/libpng
$ rm Application.mk

これで以下のような構成にします。

jni
 ├── main.cpp 
 ├── image.cpp 
 ├── image.h
 ├── Android.mk
 ├── Application.mk
 └── libpng/
     ├── ...

画像の読み込み部分

PNG を読み取って width / hegiht を格納したり、RGBA 情報を unsigned char* で返してくれるクラスを作ります。

image.h
#include <string>
#include <png.h>

class PNG
{
public:
	PNG(const std::string& file_name);
	~PNG();
	unsigned int get_width();	
	unsigned int get_height();	
	bool has_alpha();
	unsigned char* get_data();

private:
	const std::string file_name_;
	unsigned char* data_;
	png_uint_32 width_, height_;
	int bit_depth_, color_type_, interlace_type_;
	int compression_type_, filter_method_;
};
image.cpp
#include <image.h>
#include <android/log.h>

#define LOG_TAG ("PNG")
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO,  LOG_TAG, __VA_ARGS__))
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN,  LOG_TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))

PNG::PNG(const std::string& file_name) :
	file_name_(file_name), data_(nullptr),
	width_(0), height_(0),
	bit_depth_(0), color_type_(0), interlace_type_(0),
	compression_type_(0), filter_method_(0)
{
	FILE* fp = fopen(file_name_.c_str(), "rb");
	if (fp == nullptr) {
		LOGE("%s is not found.", file_name_.c_str());
		fclose(fp);
		return;
	}

	png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
	if (png == nullptr) {
		LOGE("png_create_read_struct error.");
		fclose(fp);
		return;
	}

	png_infop info = png_create_info_struct(png);
	if (info == nullptr) {
		LOGE("png_create_info_struct error.");
		png_destroy_read_struct(&png, nullptr, nullptr);
		fclose(fp);
		return;
	}

	if (setjmp(png_jmpbuf(png))) {
		LOGE("png_jmpbuf error.");
		png_destroy_read_struct(&png, &info, nullptr);
		fclose(fp);
		return;
	}

	png_init_io(png, fp);

	unsigned int sig_bytes = 0;
	png_set_sig_bytes(png, sig_bytes);

	png_read_png(png, info, (PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND), nullptr);
	png_get_IHDR(png, info, &width_, &height_, &bit_depth_, &color_type_, &interlace_type_, &compression_type_, &filter_method_);

	unsigned int row_bytes = png_get_rowbytes(png, info);
	if (data_ != nullptr) {
		delete[] data_;
	}
	data_ = new unsigned char[row_bytes * height_];

	png_bytepp rows = png_get_rows(png, info);
	for (int i = 0; i < height_; ++i) {
		memcpy(data_ + (row_bytes * i), rows[i], row_bytes);
	}

	png_destroy_read_struct(&png, &info, nullptr);
}

PNG::~PNG()
{
	if (data_) delete[] data_;
}

unsigned int PNG::get_width()
{
	return width_;
}

unsigned int PNG::get_height()
{
	return height_;
}

unsigned char* PNG::get_data()
{
	return data_;
}

bool PNG::has_alpha()
{
	if (color_type_ == PNG_COLOR_TYPE_RGBA) {
		return true;
	}
	return false;
}

OpenGL 描画部分

#include <string>

#include <EGL/egl.h>
#include <GLES/gl.h>
#include <GLES2/gl2.h>

#include <android/log.h>
#include <android_native_app_glue.h>

#include <image.h>

#define LOG_TAG ("gles2ndtest")
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO,  LOG_TAG, __VA_ARGS__))
#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN,  LOG_TAG, __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))

using namespace std;

struct engine
{
	android_app* app;
	EGLDisplay display;
	EGLSurface surface;
	EGLContext context;
};

const char* vertex_shader =
	"attribute vec4 position;\n"
	"attribute vec2 texcoord;\n"
	"varying vec2 texcoordVarying;\n"
	"void main() {\n"
		"gl_Position = position;\n"
		"texcoordVarying = texcoord;\n"
	"}\n";

const char* fragment_shader =
	"precision mediump float;\n"
	"varying vec2 texcoordVarying;\n"
	"uniform sampler2D texture;\n"
	"void main() {\n"
		"gl_FragColor = texture2D(texture, texcoordVarying);\n"
	"}\n";

const float vertices[] = {
	-1.0f,  1.0f, 0.0f,
	-1.0f, -1.0f, 0.0f,
	 1.0f,  1.0f, 0.0f,
	 1.0f, -1.0f, 0.0f
};

const float texcoords[] = {
	0.0f, 0.0f,
	0.0f, 1.0f,
	1.0f, 0.0f,
	1.0f, 1.0f
};

GLuint program, position, texcoord;
GLuint textures[1];

GLuint load_shader(GLenum shader_type, const char* source)
{
	GLuint shader = glCreateShader(shader_type);
	if (shader) {
		glShaderSource(shader, 1, &source, nullptr);
		glCompileShader(shader);
		GLint compiled = 0;
		glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
		if (!compiled) {
			GLint length = 0;
			glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
			if (length) {
				std::string log(static_cast<size_t>(length), ' ');
				glGetShaderInfoLog(shader, length, nullptr, &log[0]);
				LOGE("Could not compile shader %d:\n%s\n", shader_type, log.c_str());
				glDeleteShader(shader);
				shader = 0;
			}
		}
	}
	return shader;
}

GLuint create_program(const char* vertex_source, const char* fragment_source)
{
    GLuint vertex_shader = load_shader(GL_VERTEX_SHADER, vertex_source);
	if (vertex_shader == 0) return 0;
    GLuint pixel_shader  = load_shader(GL_FRAGMENT_SHADER, fragment_source);
	if (pixel_shader  == 0) return 0;

    GLuint program = glCreateProgram();
	glAttachShader(program, vertex_shader);
	glAttachShader(program, pixel_shader);
	glLinkProgram(program);
	GLint link_status = GL_FALSE;
	glGetProgramiv(program, GL_LINK_STATUS, &link_status);
	if (link_status != GL_TRUE) {
		GLint length = 0;
		glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
		if (length) {
				std::string log(static_cast<size_t>(length), ' ');
			glGetProgramInfoLog(program, length, nullptr, &log[0]);
			LOGE("Could not link program:\n%s\n", log.c_str());
		}
		glDeleteProgram(program);
		program = 0;
	}
    return program;
}

void load_png()
{
    program = create_program(vertex_shader, fragment_shader);
	if (program == 0) return;
	glUseProgram(program);

	position = glGetAttribLocation(program, "position");
	glEnableVertexAttribArray(position);

	texcoord = glGetAttribLocation(program, "texcoord");
	glEnableVertexAttribArray(texcoord);

	textures[0] = glGetUniformLocation(program, "texture");
	glGenTextures(1, textures);
	glBindTexture(GL_TEXTURE_2D, textures[0]);

	PNG png("/sdcard/tmp/hoge.png");
	LOGD("Image: alpha:%d width:%dpx height%dpx",
			png.has_alpha(), png.get_width(), png.get_height());

	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage2D(GL_TEXTURE_2D, 0, png.has_alpha() ? GL_RGBA : GL_RGB,
			png.get_width(), png.get_height(),
			0, png.has_alpha() ? GL_RGBA : GL_RGB,
			GL_UNSIGNED_BYTE, png.get_data());
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}

int init(engine* e)
{
	const EGLint attribs[] = {
		EGL_BLUE_SIZE,       8,
		EGL_GREEN_SIZE,      8,
		EGL_RED_SIZE,        8,
		EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
		EGL_NONE
	};
	const EGLint context_attribs[] = {
		EGL_CONTEXT_CLIENT_VERSION, 2,
		EGL_NONE
	};

	EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
	eglInitialize(display, 0, 0);

	EGLConfig config;
	EGLint num_configs;
	eglChooseConfig(display, attribs, &config, 1, &num_configs);

	EGLint format;
	eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format);
	ANativeWindow_setBuffersGeometry(e->app->window, 0, 0, format);

	EGLSurface surface = eglCreateWindowSurface(display, config, e->app->window, nullptr);
	EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs);

	if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
		return -1;
	}

	EGLint w, h;
	eglQuerySurface(display, surface, EGL_WIDTH,  &w);
	eglQuerySurface(display, surface, EGL_HEIGHT, &h);

	e->display = display;
	e->surface = surface;
	e->context = context;

	glViewport(0, 0, w, h);
}

void draw(engine* e)
{
	glClearColor(0.3f, 0.3f, 0.3f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT);

	LOGD("glBindTexture");
	glBindTexture(GL_TEXTURE_2D, textures[0]);
	LOGD("glVertexAttribPointer");
    glVertexAttribPointer(texcoord, 2, GL_FLOAT, GL_FALSE, 0, texcoords);
	LOGD("glVertexAttribPointer");
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 0, vertices);
	LOGD("glDrawArra");
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

	eglSwapBuffers(e->display, e->surface);
}

void android_main(android_app* state)
{
	app_dummy();

	engine e;
	state->userData = &e;
	state->onAppCmd = [](android_app* app, int32_t cmd) {
		auto e = static_cast<engine*>(app->userData);
		switch (cmd) {
			case APP_CMD_INIT_WINDOW:
				init(e);
				load_png();
				draw(e);
				break;
		}
	};
	e.app = state;

	while (1) {
		int ident, events;
		android_poll_source* source;
		while ((ident=ALooper_pollAll(0, nullptr, &events, (void**)&source)) >= 0) {
			if (source != nullptr) {
				source->process(state, source);
			}
			if (state->destroyRequested != 0) {
				if (e.display != EGL_NO_DISPLAY) {
					eglMakeCurrent(e.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
					if (e.context != EGL_NO_CONTEXT) {
						eglDestroyContext(e.display, e.context);
					}
					if (e.surface != EGL_NO_SURFACE) {
						eglDestroySurface(e.display, e.surface);
					}
					eglTerminate(e.display);
				}
				e.display = EGL_NO_DISPLAY;
				e.context = EGL_NO_CONTEXT;
				e.surface = EGL_NO_SURFACE;
				return;
			}
		}
	}
}

Makefile

Application.mk
APP_PLATFORM := android-10
APP_OPTIM    := debug
APP_ABI      := armeabi armeabi-v7a
APP_STL      := gnustl_static
Android.mk
TOP_LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

include $(TOP_LOCAL_PATH)/libpng/src/Android.mk

LOCAL_PATH := $(TOP_LOCAL_PATH)
include $(CLEAR_VARS)

LOCAL_MODULE     := gles2ndktest
LOCAL_CFLAGS     := -std=c++11
LOCAL_SRC_FILES  := $(wildcard *.cpp)
LOCAL_C_INCLUDES := $(LOCAL_PATH)/libpng/src/.
LOCAL_LDLIBS     := -lz -llog -landroid -lEGL -lGLESv1_CM -lGLESv2
LOCAL_STATIC_LIBRARIES := android_native_app_glue libpng

include $(BUILD_SHARED_LIBRARY)

$(call import-module,android/native_app_glue)

結果

PNG を SD カードへコピーします。

$ adb shell mkdir /sdcard/tmp
$ adb push hoge.png /sdcard/tmp/hoge.png


おわりに

本当は Freeimage 利用したかったのですが上手く行かなかったので…、上手く行ったら記事書きます。