iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
夏休み期間中という事で席に若干の余裕があった勉強会だった。
発表は「Swiftでアプリを再実装」と「新しいIBキーワード」、「Markdownパーサを比較してみる」の三本。時間に余裕があった為、色々と議論できた事と、直に役立った情報が得られたのが嬉しかった。
以前、GLKitを使ったOpenGLを試した事があるが、あのときは、OpenGL ES 1.1だった。今回は、OpenGL ES 2.0だ。
参考図書『OpenGL ES 2.0 Programming Guide』の頭に、「Hello Triangle Example」というサンプルコードが紹介されている。それをGLKitを使って実装してみた。
プロジェクトは、XcodeのiOS/Application/OpenGL Gameで生成されるコードを残す形の実装となっている。
頂点フェーダは以下の内容に書き直す。
/* 頂点バッファvPositionを入力バッファに配置 */
attribute vec4 vPosition;
void main()
{
/* vPositionを頂点座標に設定*/
gl_Position = vPosition;
}
フラグメント・フェーダは以下の内容に書き直す。
/* precision宣言 16bit(half) */
precision mediump float;
void main()
{
/* 赤色を設定 */
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
GLKViewControllerのサブクラスViewControllerは以下のとおり。
#define BUFFER_OFFSET(i) ((char *)NULL + (i))
GLfloat gVVertexData[] = {
0.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
@interface ViewController () {
GLuint _program;
GLuint _vertexArray;
GLuint _vertexBuffer;
}
@property (strong, nonatomic) EAGLContext *context;
@property (strong, nonatomic) GLKBaseEffect *effect;
- (void)setupGL;
- (void)tearDownGL;
- (BOOL)loadShaders;
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file;
- (BOOL)linkProgram:(GLuint)prog;
- (BOOL)validateProgram:(GLuint)prog;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
/* OpenGL ES 2.0コンテキストを生成 */
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!self.context) {
DBGMSG(@"Failed to create ES context");
}
GLKView *view = (GLKView *)self.view;
view.context = self.context;
[self setupGL];
}
- (void)dealloc
{
[self tearDownGL];
if ([EAGLContext currentContext] == self.context) {
[EAGLContext setCurrentContext:nil];
}
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
if ([self isViewLoaded] && ([[self view] window] == nil)) {
self.view = nil;
[self tearDownGL];
if ([EAGLContext currentContext] == self.context) {
[EAGLContext setCurrentContext:nil];
}
self.context = nil;
}
// Dispose of any resources that can be recreated.
}
- (void)setupGL
{
/* コンテキストを設定 */
[EAGLContext setCurrentContext:self.context];
[self loadShaders];
/* 頂点配列を生成し結合する */
glGenVertexArraysOES(1, &_vertexArray);
glBindVertexArrayOES(_vertexArray);
/* バッファオブジェクトを生成し、結合後、頂点データを設定 */
glGenBuffers(1, &_vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(gVVertexData), gVVertexData, GL_STATIC_DRAW);
/* 頂点属性配列を有効化し、バッファと関連づける */
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
/* 頂点配列の結合を解除 */
glBindVertexArrayOES(0);
}
- (void)tearDownGL
{
[EAGLContext setCurrentContext:self.context];
glDeleteBuffers(1, &_vertexBuffer);
glDeleteVertexArraysOES(1, &_vertexArray);
self.effect = nil;
if (_program) {
glDeleteProgram(_program);
_program = 0;
}
}
#pragma mark - GLKView and GLKViewController delegate methods
/* 今回は動かないので空。 */
- (void)update
{
}
/* 描画。GLKViewデリゲートのメソッド。 */
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
/* カラーバッファをクリア */
glClear(GL_COLOR_BUFFER_BIT);
/* シャーダが含まれるプログラムオブジェクトを設定 */
glUseProgram(_program);
/* 頂点配列を結合する */
glBindVertexArrayOES(_vertexArray);
/* GL_TRIANGLESプリミティブで描画 */
glDrawArrays(GL_TRIANGLES, 0, 3);
}
#pragma mark - OpenGL ES 2 shader compilation
- (BOOL)loadShaders
{
GLuint vertShader, fragShader;
NSString *vertShaderPathname, *fragShaderPathname;
/* プログラム・オブジェクトの生成 */
_program = glCreateProgram();
/* 頂点シェーダとフラグメント・シェーダのファイルを読み込み、コンパイルする */
// Load the vertex/fragment shaders
vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"];
if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) {
DBGMSG(@"Failed to compile vertex shader");
return NO;
}
fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"];
if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) {
DBGMSG(@"Failed to compile fragment shader");
return NO;
}
/* シェーダ・オブジェクトを登録 */
glAttachShader(_program, vertShader);
glAttachShader(_program, fragShader);
/* 頂点シェーダ属性をGLKVertexAttribPosition(0)に結合する */
glBindAttribLocation(_program, GLKVertexAttribPosition, "vPosition");
/* プログラム・オブジェクトに関連づける */
if (![self linkProgram:_program]) {
DBGMSG(@"Failed to link program: %d", _program);
if (vertShader) {
glDeleteShader(vertShader);
vertShader = 0;
}
if (fragShader) {
glDeleteShader(fragShader);
fragShader = 0;
}
if (_program) {
glDeleteProgram(_program);
_program = 0;
}
return NO;
}
// Release vertex and fragment shaders.
if (vertShader) {
glDetachShader(_program, vertShader);
glDeleteShader(vertShader);
}
if (fragShader) {
glDetachShader(_program, fragShader);
glDeleteShader(fragShader);
}
return YES;
}
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
GLint status;
const GLchar *source;
source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String];
if (!source) {
DBGMSG(@"Failed to load vertex shader");
return NO;
}
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &source, NULL);
glCompileShader(*shader);
#if defined(DEBUG)
GLint logLength;
glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetShaderInfoLog(*shader, logLength, &logLength, log);
DBGMSG(@"Shader compile log:\n%s", log);
free(log);
}
#endif
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
if (status == 0) {
glDeleteShader(*shader);
return NO;
}
return YES;
}
- (BOOL)linkProgram:(GLuint)prog
{
GLint status;
// Link the program
glLinkProgram(prog);
#if defined(DEBUG)
GLint logLength;
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(prog, logLength, &logLength, log);
DBGMSG(@"Program link log:\n%s", log);
free(log);
}
#endif
// Check the link status
glGetProgramiv(prog, GL_LINK_STATUS, &status);
if (status == 0) {
GLint infoLen = 0;
glGetProgramiv(_program, GL_INFO_LOG_LENGTH, &infoLen);
if (1 < infoLen) {
char *infoLog = malloc(sizeof(char) * infoLen);
glGetProgramInfoLog(_program, infoLen, NULL, infoLog);
DBGMSG(@"%s Error linking program:\n%s\n", __func__, infoLog);
free(infoLog);
}
return NO;
}
return YES;
}
- (BOOL)validateProgram:(GLuint)prog
{
GLint logLength, status;
glValidateProgram(prog);
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(prog, logLength, &logLength, log);
DBGMSG(@"Program validate log:\n%s", log);
free(log);
}
glGetProgramiv(prog, GL_VALIDATE_STATUS, &status);
if (status == 0) {
return NO;
}
return YES;
}
@end
複数の画像と音声データから動画の生成を試している。画像から動画が動いたので当初の目的は達成されていないが記事にする。
基本的に、参考情報のブログ記事をまねさせて貰っている。ありがとう。
独自にカスタマイズしてハマったところ。一つは、AVAssetWriterのインスタンスはプロパティで保持しておいて処理が完了するまでリリースされないようにしないと、処理が途中で終わる。出力先の動画ファイル名は同じものにしているので、既に存在していたら削除しているが、ファイル出力の直前に削除すると、出力が失敗した。画像にノイズが入るバグがあるが解決していない。
渡された画像を一秒間隔でスライドショーしているが、この裏で音声データを埋め込みたい。その方法は調査中だ。
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
@property (strong, nonatomic) AVAssetWriter *movieAssetWriter;
@property (readonly) NSArray *images;
@property (readonly) CGSize imageSize;
@property (readonly) NSString *moviePath;
- (void)_photoToMovie;
- (CVPixelBufferRef)_pixelBufferFromCGImage:(CGImageRef)image;
- (void)_removeMovieFile;
- (void)_saveMovie;
- (void)_completionHandlerWithVideo:(NSString *)videoPath
didFinishSavingWithError:(NSError *)error
contextInfo:(void *)contextInfo;
@end
@implementation ViewController
- (IBAction)photoToMovie:(id)sender
{
[self _photoToMovie];
}
- (NSArray *)images
{
return @[[UIImage imageNamed:@"one"],
[UIImage imageNamed:@"two"],
[UIImage imageNamed:@"three"],
[UIImage imageNamed:@"four"]];
}
-(CGSize)imageSize
{
/* 全画像のサイズが同一を想定している */
return ((UIImage *)self.images[0]).size;
}
- (NSString *)moviePath
{
/* 出力ファイルのパスを作成 */
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentPath = paths[0];
NSString *moviePath = [documentPath stringByAppendingPathComponent:@"photo2movie.mov"];
DBGMSG(@"%s moviePath(%@)", __func__, moviePath);
return moviePath;
}
- (void)_photoToMovie
{
/* 既存の動画ファイルを削除 */
[self _removeMovieFile];
/* 動画出力インスタンスを生成 */
NSError *error = nil;
self.movieAssetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:self.moviePath]
fileType:AVFileTypeQuickTimeMovie
error:&error];
if (error) {
DBGMSG(@"%@", [error localizedDescription]);
return;
}
/* AVAssetWriterInputはCMSampleBufferRefでデータを受け取る */
AVAssetWriterInput *assetWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
outputSettings:@{AVVideoCodecKey:AVVideoCodecH264,
AVVideoWidthKey:@(self.imageSize.width),
AVVideoHeightKey:@(self.imageSize.height)}];
[self.movieAssetWriter addInput:assetWriterInput];
/* AVAssetWriterInputPixelBufferAdaptorを使うとCVPixelBufferRefでデータを受け取れる */
AVAssetWriterInputPixelBufferAdaptor *assetWriterInputPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:assetWriterInput
sourcePixelBufferAttributes:@{(NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32ARGB),
(NSString *)kCVPixelBufferWidthKey:@(self.imageSize.width),
(NSString *)kCVPixelBufferHeightKey:@(self.imageSize.height)}];
assetWriterInput.expectsMediaDataInRealTime = YES;
/* 動画生成開始 */
if (![self.movieAssetWriter startWriting]) {
DBGMSG(@"Failed to start writing.");
return;
}
[self.movieAssetWriter startSessionAtSourceTime:kCMTimeZero];
int frameCount = 0;
int durationForEachImage = 1;
int32_t fps = 1;
for (UIImage *image in self.images) {
if (assetWriterInputPixelBufferAdaptor.assetWriterInput.readyForMoreMediaData) {
CMTime frameTime = CMTimeMake((int64_t)frameCount * fps * durationForEachImage, fps);
CVPixelBufferRef pixelBufferRef = [self _pixelBufferFromCGImage:image.CGImage];
if (![assetWriterInputPixelBufferAdaptor appendPixelBuffer:pixelBufferRef withPresentationTime:frameTime]) {
DBGMSG(@"Failed to append buffer. [image : %@]", image);
}
if(pixelBufferRef) {
CVBufferRelease(pixelBufferRef);
}
frameCount++;
}
}
// 動画生成終了
[assetWriterInput markAsFinished];
[self _saveMovie];
CVPixelBufferPoolRelease(assetWriterInputPixelBufferAdaptor.pixelBufferPool);
}
- (CVPixelBufferRef)_pixelBufferFromCGImage:(CGImageRef)imageRef
{
NSDictionary *options = @{(NSString *)kCVPixelBufferCGImageCompatibilityKey:@(YES),
(NSString *)kCVPixelBufferCGBitmapContextCompatibilityKey:@(YES)};
CVPixelBufferRef pxbuffer = NULL;
CVPixelBufferCreate(kCFAllocatorDefault,
CGImageGetWidth(imageRef),
CGImageGetHeight(imageRef),
kCVPixelFormatType_32ARGB,
(__bridge CFDictionaryRef)options,
&pxbuffer);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pxdata,
CGImageGetWidth(imageRef),
CGImageGetHeight(imageRef),
8,
4 * CGImageGetWidth(imageRef),
rgbColorSpace,
(CGBitmapInfo)kCGImageAlphaNoneSkipFirst);
CGContextConcatCTM(context, CGAffineTransformMakeRotation(0));
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)), imageRef);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
- (void)_removeMovieFile
{
/* 既存の動画ファイルを削除 */
NSError *error;
if ([[NSFileManager defaultManager] fileExistsAtPath:self.moviePath]) {
[[NSFileManager defaultManager] removeItemAtPath:self.moviePath error:&error];
if (error) {
DBGMSG(@"%@", [error localizedDescription]);
}
}
}
- (void)_saveMovie
{
/* 書き込み */
[self.movieAssetWriter finishWritingWithCompletionHandler:^{
DBGMSG(@"Finish writing!");
self.movieAssetWriter = nil;
UISaveVideoAtPathToSavedPhotosAlbum(self.moviePath, self, @selector(_completionHandlerWithVideo:didFinishSavingWithError:contextInfo:), NULL);
}];
}
- (void)_completionHandlerWithVideo:(NSString *)videoPath
didFinishSavingWithError:(NSError *)error
contextInfo:(void *)contextInfo
{
DBGMSG(@"%s error:%@", __func__, error);
}
@end
自動制御の勉強をする為、gnuplotをインストールした。その手順を自分自身への備忘録として投稿する。
_ X11
_ gnuplot
組み込みのreadlineを使用する事にした。また、コマンド履歴を有効にした。
$ tar -xvzf gnuplot-4.6.5.tar.gz
$ cd gnuplot-4.6.5
$ ./configure --with-readline=builtin --enable-history-file
$ make
$ sudo make install