diff --git a/Samples/GVRKit/GVRHeadPose.h b/Samples/GVRKit/GVRHeadPose.h new file mode 100644 index 0000000..1f0467e --- /dev/null +++ b/Samples/GVRKit/GVRHeadPose.h @@ -0,0 +1,86 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" +#include "vr/gvr/capi/include/gvr.h" +#include "vr/gvr/capi/include/gvr_audio.h" +#include "vr/gvr/capi/include/gvr_types.h" +#pragma clang diagnostic pop + +/** + * @enum GVREye + * + * Enumeration of the left and right eyes, used to identify the correct + * rendering parameters needed for stereo rendering. + */ +typedef NS_ENUM(NSInteger, GVREye) { + kGVRLeftEye = GVR_LEFT_EYE, /**< Left eye. */ + kGVRRightEye = GVR_RIGHT_EYE, /**< Right eye. */ + kGVRCenterEye, /**< Center eye. */ +}; + +/** + * Defines a struct to hold half field of view angles, in degrees. + */ +typedef struct { + CGFloat left; + CGFloat right; + CGFloat bottom; + CGFloat top; +} GVRFieldOfView; + +/** + * Defines a class that provides current head pose transforms. + */ +@interface GVRHeadPose : NSObject + +/** The current |GVREye| eye being rendered with the head pose. */ +@property(nonatomic, readonly) GVREye eye; + +/** The head pose transform matrix. Includes manual yaw and pitch rotations. */ +@property(nonatomic, readonly) GLKMatrix4 headTransform; + +/** The eye transform relative to the head transform. For monoscopic this is identity matrix. */ +@property(nonatomic, readonly) GLKMatrix4 eyeTransform; + +/** The view (camera) transform that is the composite of head and eye transforms. */ +@property(nonatomic, readonly) GLKMatrix4 viewTransform; + +/** The projection transform computed from ::setProjectionMatrixWithNear:Far: */ +@property(nonatomic, readonly) GLKMatrix4 projectionTransform; + +/** The current display viewport. */ +@property(nonatomic, readonly) CGRect viewport; + +/** The current field of view. This depends on paired Cardboard viewer in stereoscopic mode. */ +@property(nonatomic, readonly) GVRFieldOfView fieldOfView; + +/** The timestamp when this frame will be presented. */ +@property(nonatomic, readonly) NSTimeInterval nextFrameTime; + +/** + * Computes the projection matrix with near and far planes and is accessed from projectionTransform + * property. + */ +- (void)setProjectionMatrixWithNear:(CGFloat)near far:(CGFloat)far; + +/** Returns the projection matrix given near and far planes. */ +- (GLKMatrix4)projectionMatrixWithNear:(CGFloat)near far:(CGFloat)far; + +@end diff --git a/Samples/GVRKit/GVRHeadPose.mm b/Samples/GVRKit/GVRHeadPose.mm new file mode 100644 index 0000000..d8e0dad --- /dev/null +++ b/Samples/GVRKit/GVRHeadPose.mm @@ -0,0 +1,203 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRHeadPose.h" + +// For monoscopic rendering, define 45 degree half angle horizontally and vertically. +static const float kMonoFieldOfView = 45.0f; + +namespace { + +static void GVRMatrixToGLKMatrix4(const gvr::Mat4f &matrix, GLKMatrix4 *glkMatrix) { + // Note that this performs a *tranpose* to a column-major matrix array, as + // expected by GL. + float result[16]; + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result[j * 4 + i] = matrix.m[i][j]; + } + } + *glkMatrix = GLKMatrix4MakeWithArray(result); +} + +static gvr::Recti CalculatePixelSpaceRect(const gvr::Sizei &size, const gvr::Rectf &source_rect) { + float width = static_cast(size.width); + float height = static_cast(size.height); + gvr::Rectf rect = {source_rect.left * width, source_rect.right * width, + source_rect.bottom * height, source_rect.top * height}; + gvr::Recti result = {static_cast(rect.left), static_cast(rect.right), + static_cast(rect.bottom), static_cast(rect.top)}; + return result; +} + +static gvr::Mat4f PerspectiveMatrixFromView(const GVRFieldOfView &fov, float z_near, float z_far) { + gvr::Mat4f result; + const float x_left = -std::tan(fov.left * M_PI / 180.0f) * z_near; + const float x_right = std::tan(fov.right * M_PI / 180.0f) * z_near; + const float y_bottom = -std::tan(fov.bottom * M_PI / 180.0f) * z_near; + const float y_top = std::tan(fov.top * M_PI / 180.0f) * z_near; + const float zero = 0.0f; + + assert(x_left < x_right && y_bottom < y_top && z_near < z_far && z_near > zero && z_far > zero); + const float X = (2 * z_near) / (x_right - x_left); + const float Y = (2 * z_near) / (y_top - y_bottom); + const float A = (x_right + x_left) / (x_right - x_left); + const float B = (y_top + y_bottom) / (y_top - y_bottom); + const float C = (z_near + z_far) / (z_near - z_far); + const float D = (2 * z_near * z_far) / (z_near - z_far); + + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + result.m[i][j] = 0.0f; + } + } + result.m[0][0] = X; + result.m[0][2] = A; + result.m[1][1] = Y; + result.m[1][2] = B; + result.m[2][2] = C; + result.m[2][3] = D; + result.m[3][2] = -1; + + return result; +} + +} // namespace + +@implementation GVRHeadPose { + UIInterfaceOrientation _orientation; + gvr::Sizei _renderSize; + CGFloat _yaw; + CGFloat _pitch; +} + +- (instancetype)init { + if (self = [super init]) { + _yaw = _pitch = 0.0f; + } + return self; +} + +- (void)setHeadPose:(gvr::Mat4f &)headPose + renderSize:(gvr::Sizei)renderSize + orientation:(UIInterfaceOrientation)orientation + nextFrameTime:(NSTimeInterval)nextFrameTime { + // Next frame time. + _nextFrameTime = nextFrameTime; + + // Eye. + _eye = kGVRCenterEye; + + // Head transform. + GVRMatrixToGLKMatrix4(headPose, &_headTransform); + + // Apply yaw rotation. + _headTransform = GLKMatrix4Multiply(_headTransform, GLKMatrix4MakeYRotation(_yaw)); + + // For pitch rotation we have to take the interface orientation into account. GVR always draws in + // landscape right orientation, where the pitch is correctly applied to X axis. But in portrait + // mode, we apply the pitch to the Y axis. + if (UIInterfaceOrientationIsLandscape(_orientation)) { + _headTransform = GLKMatrix4Multiply(GLKMatrix4MakeXRotation(_pitch), _headTransform); + } else { + _headTransform = GLKMatrix4Multiply(GLKMatrix4MakeYRotation(_pitch), _headTransform); + } + + // Eye transform. + _eyeTransform = GLKMatrix4Identity; + + // View transform. + _viewTransform = _headTransform; + + // Viewport. + _renderSize = renderSize; + _viewport = CGRectMake(0, 0, _renderSize.width, _renderSize.height); + + // Field of view. + const float aspect_ratio = (float)_renderSize.width / (float)_renderSize.height; + float vertFov = kMonoFieldOfView; + float horizFov = + std::atan(aspect_ratio * std::tan(kMonoFieldOfView * M_PI / 180.0f)) * 180 / M_PI; + _fieldOfView = {horizFov, horizFov, vertFov, vertFov}; + + _orientation = orientation; +} + +- (void)setEyePose:(gvr::Mat4f &)eyePose + forEye:(GVREye)eye + bufferViewport:(gvr::BufferViewport *)bufferViewport { + // Eye. + _eye = eye; + + // Eye transform. + GVRMatrixToGLKMatrix4(eyePose, &_eyeTransform); + + // View transform. + _viewTransform = GLKMatrix4Multiply(_eyeTransform, _headTransform); + + // Viewport. + gvr::Recti pixel_rect = CalculatePixelSpaceRect(_renderSize, bufferViewport->GetSourceUv()); + _viewport = CGRectMake(pixel_rect.left, + pixel_rect.bottom, + pixel_rect.right - pixel_rect.left, + pixel_rect.top - pixel_rect.bottom); + + // Field of view. + const gvr::Rectf &fov = bufferViewport->GetSourceFov(); + _fieldOfView = {fov.left, fov.right, fov.bottom, fov.top}; +} + +- (void)addToHeadRotationYaw:(CGFloat)yaw andPitch:(CGFloat)pitch { + _yaw += yaw; + _pitch += pitch; +} + +- (void)resetHeadRotation { + _yaw = _pitch = 0.0f; +} + +- (void)setProjectionMatrixWithNear:(CGFloat)near far:(CGFloat)far { + _projectionTransform = [self projectionMatrixWithNear:near far:far]; +} + +- (GLKMatrix4)projectionMatrixWithNear:(CGFloat)near far:(CGFloat)far { + gvr::Mat4f perspective = PerspectiveMatrixFromView(_fieldOfView, near, far); + GLKMatrix4 transform; + GVRMatrixToGLKMatrix4(perspective, &transform); + return GLKMatrix4Multiply(transform, [self interfaceRotationFromOrientation:_orientation]); +} + +- (GLKMatrix4)interfaceRotationFromOrientation:(UIInterfaceOrientation)orientation { + // Compute interface rotation matrix based on interface orientation. + switch (orientation) { + case UIInterfaceOrientationPortrait: + return GLKMatrix4MakeZRotation(-M_PI_2); + break; + case UIInterfaceOrientationPortraitUpsideDown: + return GLKMatrix4MakeZRotation(M_PI_2); + break; + case UIInterfaceOrientationLandscapeLeft: + return GLKMatrix4MakeZRotation(M_PI); + break; + + case UIInterfaceOrientationLandscapeRight: + default: + return GLKMatrix4Identity; + break; + } +} + +@end diff --git a/Samples/GVRKit/GVRImageRenderer.h b/Samples/GVRKit/GVRImageRenderer.h new file mode 100644 index 0000000..05a44fb --- /dev/null +++ b/Samples/GVRKit/GVRImageRenderer.h @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRTextureRenderer.h" + +/** Defines a texture renderer for images. */ +@interface GVRImageRenderer : GVRTextureRenderer + +/** Initialize texture renderer from given image. */ +- (instancetype)initWithImage:(UIImage *)image; + +/** Initialize texture renderer from an image from the given path. */ +- (instancetype)initWithContentsOfFile:(NSString *)path; + +@end diff --git a/Samples/GVRKit/GVRImageRenderer.mm b/Samples/GVRKit/GVRImageRenderer.mm new file mode 100644 index 0000000..9eade74 --- /dev/null +++ b/Samples/GVRKit/GVRImageRenderer.mm @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRImageRenderer.h" + +@implementation GVRImageRenderer { + dispatch_block_t _textureLoader; +} + +- (instancetype)initWithContentsOfFile:(NSString *)path { + if (self = [super init]) { + // Defer loading until the renderer is initialized. + __weak __typeof(self) weakSelf = self; + _textureLoader = ^{ + GLKTextureLoader *loader = + [[GLKTextureLoader alloc] initWithSharegroup:EAGLContext.currentContext.sharegroup]; + [loader textureWithContentsOfFile:path + options:nil + queue:NULL + completionHandler:^(GLKTextureInfo *textureInfo, NSError *outError) { + __strong __typeof(self) strongSelf = weakSelf; + if (textureInfo) { + // Allow non-power of 2 sized textures. + glBindTexture(textureInfo.target, textureInfo.name); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + [strongSelf setImageTextureId:textureInfo.name]; + } + }]; + }; + } + return self; +} + +- (instancetype)initWithImage:(UIImage *)image { + if (self = [super init]) { + // Defer loading until the renderer is initialized. + __weak __typeof(self) weakSelf = self; + _textureLoader = ^{ + GLKTextureLoader *loader = + [[GLKTextureLoader alloc] initWithSharegroup:EAGLContext.currentContext.sharegroup]; + [loader textureWithCGImage:image.CGImage + options:nil + queue:NULL + completionHandler:^(GLKTextureInfo *textureInfo, NSError *outError) { + __strong __typeof(self) strongSelf = weakSelf; + if (textureInfo) { + // Allow non-power of 2 sized textures. + glBindTexture(textureInfo.target, textureInfo.name); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + [strongSelf setImageTextureId:textureInfo.name]; + } + }]; + }; + } + return self; +} + +#pragma mark - GVRTextureRenderer + +- (void)initializeGl { + [super initializeGl]; + // Load the texture once GL is initialized. + if (_textureLoader) { + _textureLoader(); + } +} + +@end diff --git a/Samples/GVRKit/GVRKit.h b/Samples/GVRKit/GVRKit.h new file mode 100644 index 0000000..d4087b3 --- /dev/null +++ b/Samples/GVRKit/GVRKit.h @@ -0,0 +1,11 @@ +// Umbrella header file for all GVRKit headers. +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/Samples/GVRKit/GVRRenderer.h b/Samples/GVRKit/GVRRenderer.h new file mode 100644 index 0000000..9f45df5 --- /dev/null +++ b/Samples/GVRKit/GVRRenderer.h @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "GVRHeadPose.h" + +/** + * Defines a class that provides rendering of monoscopic and stereoscopic (VR) + * content to display of the mobile device. The property ::VRModeEnabled controls + * the display of monoscopic vs. stereoscopic content. + * + * This class is meant to be subclassed in order to implement application specific rendering. + * Subclasses should override ::update:, ::draw and ::handleTrigger methods. + * + * Call ::initializeGl and ::clearGl before and after rendering on a valid GL context. Application + * should call ::drawFrame: on every iteration of the render loop on a valid GL context. The + * ::pause method should be called to pause head tracking and rendering. + * + * Although this class makes no assumption of threading context, it has been tested to be used + * only on the main thread. + * + * Applications can manually add yaw and pitch rotation to the current head pose. + */ +@interface GVRRenderer : NSObject + +/** + * The vrModeEnabled property allows switching between monoscopic and stereoscopic (VR) rendering. + * Stereoscopic rendering should be enabled only in fullscreen mode. + */ +@property(nonatomic) BOOL VRModeEnabled; + + +/** Initializes GL resources used by GVR. This should be called on a valid GL context. */ +- (void)initializeGl; + +/** Releases GL resources used by GVR. This should be called on a valid GL context. */ +- (void)clearGl; + +/** + * Draws the monoscopic or stereoscopic frame depending upon VRModeEnabled property. Calls + * ::draw:headPose method of subclasses to perform the actual drawing. This method should + * be called on every iteration of the render loop. + * + * @param nextFrameTime The timestamp of when the frame will be rendered to the display. + */ +- (void)drawFrame:(NSTimeInterval)nextFrameTime; + +/** + * Updates the rendering based on currently paired viewer profile. Call this if the user + * pairs a Cardboard viewer. + */ +- (void)refresh; + +/** + * Calls the method ::handleTriger: with the current head pose. Returns YES if + * the subclasses handled this method. + */ +- (BOOL)handleTrigger; + +/** + * Add yaw and pitch rotation to the current head pose. This should be the delta + * from the last update. Internally, this is cumulatively applied to the current + * head pose. See ::resetHeadRotation to clear additional rotation. + */ +- (void)addToHeadRotationYaw:(CGFloat)yaw andPitch:(CGFloat)pitch; + +/** + * Clears any additional yaw and pitch rotation applied to the current head pose. + * This method also recenters head tracking. + */ +- (void)resetHeadRotation; + +/** + * Call this when the render display size changes, including the current interface + * orientation. When ::VRModeEnabled is set to YES, the size should be fullscreen + * and orientation should be UIInterfaceOrientationLandscapeRight. + */ +- (void)setSize:(CGSize)size andOrientation:(UIInterfaceOrientation)orientation; + +/** + * Pauses or resumes head tracking and rendering. Subclasses should superclass's implementation. + * @param pause YES to pause and NO to resume. + */ +- (void)pause:(BOOL)pause; + +/** + * Subclasses should override this method to prepare for drawing with the + * supplied head pose. This is called for every frame, once per frame. + */ +- (void)update:(GVRHeadPose *)headPose; + +/** + * Subclasses should override this method to draw the content for the supplied + * head pose. This would be called every frame, once for monoscopic and twice for + * stereoscopic rendering. For stereoscopic rendering, the headpose will contain + * eye transform matrix. + */ +- (void)draw:(GVRHeadPose *)headPose; + +/** + * Subclasses should override this method to handle user pressing the trigger in the + * Cardboard viewer. They should return YES if the trigger action is handled. + * @param headPose The headpose used to draw the view. + */ +- (BOOL)handleTrigger:(GVRHeadPose *)headPose; + +@end diff --git a/Samples/GVRKit/GVRRenderer.mm b/Samples/GVRKit/GVRRenderer.mm new file mode 100644 index 0000000..10aa648 --- /dev/null +++ b/Samples/GVRKit/GVRRenderer.mm @@ -0,0 +1,185 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRRenderer.h" + +static const uint64_t kPredictionTimeWithoutVsyncNanos = 50000000; + +// Exposes internal methods of GVRHeadPose. +@interface GVRHeadPose (GVRInternal) + +// Set the head pose transform, orientation and render size. +- (void)setHeadPose:(gvr::Mat4f &)headPose + renderSize:(gvr::Sizei)renderSize + orientation:(UIInterfaceOrientation)orientation + nextFrameTime:(NSTimeInterval)nextFrameTime; + +// Set the eye transform for a given eye and its viewport. +- (void)setEyePose:(gvr::Mat4f &)eyePose + forEye:(GVREye)eye + bufferViewport:(gvr::BufferViewport *)bufferViewport; + +// Add yaw and pitch rotation to the head pose. +- (void)addToHeadRotationYaw:(CGFloat)yaw andPitch:(CGFloat)pitch; + +// Remove all yaw and pitch rotations from the head pose. +- (void)resetHeadRotation; + +@end + +@interface GVRRenderer () { + std::unique_ptr _gvrApi; + std::unique_ptr _viewportList; + std::unique_ptr _swapchain; + gvr::Sizei _renderSize; + gvr::Sizei _size; + UIInterfaceOrientation _orientation; + GLKMatrix4 _headRotation; + GVRHeadPose *_headPose; +} +@end + +@implementation GVRRenderer + +- (void)initializeGl { + _gvrApi = gvr::GvrApi::Create(); + _gvrApi->InitializeGl(); + + std::vector specs; + specs.push_back(_gvrApi->CreateBufferSpec()); + _renderSize = specs[0].GetSize(); + _swapchain.reset(new gvr::SwapChain(_gvrApi->CreateSwapChain(specs))); + _viewportList.reset(new gvr::BufferViewportList(_gvrApi->CreateEmptyBufferViewportList())); + + _headPose = [[GVRHeadPose alloc] init]; + _headRotation = GLKMatrix4Identity; +} + +- (void)clearGl { + _viewportList.release(); + _swapchain.release(); + _gvrApi.release(); +} + +- (void)drawFrame:(NSTimeInterval)nextFrameTime { + gvr::ClockTimePoint target_time = gvr::GvrApi::GetTimePointNow(); + target_time.monotonic_system_time_nanos += kPredictionTimeWithoutVsyncNanos; + + gvr::Mat4f gvr_head_pose = _gvrApi->GetHeadSpaceFromStartSpaceRotation(target_time); + [_headPose setHeadPose:gvr_head_pose + renderSize:(_VRModeEnabled ? _renderSize : _size) + orientation:_orientation + nextFrameTime:nextFrameTime]; + + [self update:_headPose]; + + if (!self.VRModeEnabled) { + // Draw head (center eye) for monoscopic rendering. + [self draw:_headPose]; + } else { + _viewportList->SetToRecommendedBufferViewports(); + + gvr::Frame frame = _swapchain->AcquireFrame(); + frame.BindBuffer(0); + + // Draw eyes. + gvr::BufferViewport viewport; + for (int eye = GVR_LEFT_EYE; eye <= GVR_RIGHT_EYE; eye++) { + _viewportList->GetBufferViewport(eye, &viewport); + + gvr::Mat4f eye_pose = _gvrApi->GetEyeFromHeadMatrix(static_cast(eye)); + + [_headPose setEyePose:eye_pose forEye:static_cast(eye) bufferViewport:&viewport]; + + [self draw:_headPose]; + } + + // Bind back to the default framebuffer. + frame.Unbind(); + frame.Submit(*_viewportList, gvr_head_pose); + } +} + +- (void)refresh { + if (_gvrApi) { + _gvrApi->RefreshViewerProfile(); + } +} + +- (void)addToHeadRotationYaw:(CGFloat)yaw andPitch:(CGFloat)pitch { + [_headPose addToHeadRotationYaw:yaw andPitch:pitch]; +} + +- (void)resetHeadRotation { + [_headPose resetHeadRotation]; + if (_gvrApi) { + gvr_reset_tracking(_gvrApi->GetContext()); + } +} + +- (void)setSize:(CGSize)size andOrientation:(UIInterfaceOrientation)orientation { + CGFloat scale = [GVRRenderer screenDpi]; + _size = {static_cast(size.width * scale), static_cast(size.height * scale)}; + + _orientation = orientation; +} + +- (void)pause:(BOOL)pause { + if (!_gvrApi) { + return; + } + + if (pause) { + _gvrApi->PauseTracking(); + } else { + _gvrApi->ResumeTracking(); + } +} + +- (BOOL)handleTrigger { + return [self handleTrigger:_headPose]; +} + +- (BOOL)handleTrigger:(GVRHeadPose *)headPose { + // Overridden by subclasses. + return NO; +} + +- (void)update:(GVRHeadPose *)headPose { + // Overridden by subclasses. +} + +- (void)draw:(GVRHeadPose *)headPose { + // Overridden by subclasses. +} + +#pragma mark - Private + ++ (CGFloat)screenDpi { + static dispatch_once_t onceToken; + static CGFloat scale; + dispatch_once(&onceToken, ^{ + if ([[UIScreen mainScreen] respondsToSelector:@selector(nativeScale)]) { + scale = [UIScreen mainScreen].nativeScale; + } else { + scale = [UIScreen mainScreen].scale; + } + }); + + return scale; +} + +@end diff --git a/Samples/GVRKit/GVRRendererView.h b/Samples/GVRKit/GVRRendererView.h new file mode 100644 index 0000000..5610ee7 --- /dev/null +++ b/Samples/GVRKit/GVRRendererView.h @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module" +#import "GVROverlayView.h" +#pragma clang diagnostic pop +#import "GVRRenderer.h" + +/** + * Defines a view to handle GVR rendering. It provides its own render loop using |CADisplayLink|. It + * can be initialized with an instance of |GVRRenderer|. + */ +@interface GVRRendererView : GLKView + +/** Convenience initializer with the provided renderer. If another initializer is used, the + * renderer is initialized to |GVRSceneRenderer}. + * + * @param renderer The renderer used to render the contents of the view. + */ +- (instancetype)initWithRenderer:(GVRRenderer *)renderer; + +/** The renderer used to render the contents of the view. */ +@property(nonatomic, readonly) GVRRenderer *renderer; + +/** The overlay view used to provide a UI layer for Cardboard features such as icons to switch + * between monoscopic and stereoscopic rendering, a transition view to prompt the user to insert + * the phone is the viewer and to pair to a different viewer. */ +@property(nonatomic, readonly) GVROverlayView* overlayView; + +/** Allows toggling between monoscopic and stereoscopic (VR mode) rendering. */ +@property(nonatomic) BOOL VRModeEnabled; + +/** Pause or resume the |CADisplayLink| render loop. */ +@property(nonatomic) BOOL paused; + +@end diff --git a/Samples/GVRKit/GVRRendererView.mm b/Samples/GVRKit/GVRRendererView.mm new file mode 100644 index 0000000..a3cc833 --- /dev/null +++ b/Samples/GVRKit/GVRRendererView.mm @@ -0,0 +1,229 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRRendererView.h" + +#import "GVRSceneRenderer.h" + +/** Defines a class to set the supplied EAGLContext and restore old context when destroyed. **/ +class GVRSetContext { +public: + GVRSetContext(EAGLContext *context) { + _oldContext = [EAGLContext currentContext]; + [EAGLContext setCurrentContext:context]; + } + ~GVRSetContext() { + [EAGLContext setCurrentContext:_oldContext]; + } +private: + EAGLContext *_oldContext; +}; + +@implementation GVRRendererView { + CADisplayLink *_displayLink; + BOOL _initialized; +} + +- (instancetype)init { + return [self initWithRenderer:nil]; +} + +- (instancetype)initWithRenderer:(GVRRenderer *)renderer { + if (self = [super init]) { + // Default to |GVRSceneRenderer| if no renderer is provided. + if (renderer == nil) { + renderer = [[GVRSceneRenderer alloc] init]; + } + _renderer = renderer; + + // Create an overlay view on top of the GLKView. + _overlayView = [[GVROverlayView alloc] initWithFrame:self.bounds]; + _overlayView.hidesBackButton = YES; + _overlayView.autoresizingMask = + UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:_overlayView]; + + // Add a tap gesture to handle viewer trigger action. + UITapGestureRecognizer *tapGesture = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapGLView:)]; + [self addGestureRecognizer:tapGesture]; + + // Add pan gesture to allow manual tracking. + UIPanGestureRecognizer *panGesture = + [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(didPanGLView:)]; + [self addGestureRecognizer:panGesture]; + + self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + self.drawableDepthFormat = GLKViewDrawableDepthFormat16; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillResignActive:) + name:UIApplicationWillResignActiveNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + // Shutdown GVRRenderer. + GVRSetContext context(self.context); + [_renderer clearGl]; + [self deleteDrawable]; + + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)setVRModeEnabled:(BOOL)VRModeEnabled { + if (_VRModeEnabled == VRModeEnabled) { + return; + } + + _renderer.VRModeEnabled = VRModeEnabled; + + [self willChangeValueForKey:@"VRModeEnabled"]; + _VRModeEnabled = VRModeEnabled; + [self didChangeValueForKey:@"VRModeEnabled"]; + + [self updateOverlayView]; +} + +- (void)setPaused:(BOOL)paused { + [self willChangeValueForKey:@"VRModeEnabled"]; + _paused = paused; + [self didChangeValueForKey:@"VRModeEnabled"]; + + [_renderer pause:paused]; + + _displayLink.paused = (self.superview == nil || _paused); +} + +- (void)didMoveToSuperview { + [super didMoveToSuperview]; + + // Start rendering only when added to a superview and vice versa. + if (self.superview) { + [self startRenderer]; + } else { + [self stopRenderer]; + } +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if (!_initialized) { + _initialized = YES; + // Initialize GVRRenderer. + GVRSetContext context(self.context); + [self bindDrawable]; + [_renderer initializeGl]; + } + + UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation; + [_renderer setSize:self.bounds.size andOrientation:orientation]; +} + +- (void)drawRect:(CGRect)rect { + [super drawRect:rect]; + + if (!_initialized) { + return; + } + + GVRSetContext context(self.context); + + if ([_displayLink respondsToSelector:@selector(targetTimestamp)]) { + [_renderer drawFrame:_displayLink.targetTimestamp]; + } else { + NSTimeInterval nextFrameTime = + _displayLink.timestamp + (_displayLink.duration * _displayLink.frameInterval); + [_renderer drawFrame:nextFrameTime]; + } +} + +#pragma mark - Actions + +- (void)didTapGLView:(UIPanGestureRecognizer *)panGesture { + GVRSetContext context(self.context); + // If renderer does not handle the trigger, call the delegate. + if ([_renderer handleTrigger]) { + return; + } + if ([self.overlayView.delegate respondsToSelector:@selector(didTapTriggerButton)]) { + [self.overlayView.delegate didTapTriggerButton]; + } +} + +- (void)didPanGLView:(UIPanGestureRecognizer *)panGesture { + CGPoint translation = [panGesture translationInView:self]; + [panGesture setTranslation:CGPointZero inView:self]; + + // Compute rotation from translation delta. + CGFloat yaw = GLKMathDegreesToRadians(-translation.x); + CGFloat pitch = GLKMathDegreesToRadians(-translation.y); + + [_renderer addToHeadRotationYaw:yaw andPitch:pitch]; +} + +#pragma mark - NSNotificationCenter + +- (void)applicationWillResignActive:(NSNotification *)notification { + self.paused = YES; +} + +- (void)applicationDidBecomeActive:(NSNotification *)notification { + self.paused = NO; +} + +#pragma mark - Private + +- (void)updateOverlayView { + // Transition view is always shown when VR mode is toggled ON. + _overlayView.hidesTransitionView = _overlayView.hidesTransitionView || !_VRModeEnabled; + _overlayView.hidesSettingsButton = !_VRModeEnabled; + _overlayView.hidesAlignmentMarker = !_VRModeEnabled; + _overlayView.hidesFullscreenButton = !_VRModeEnabled; + _overlayView.hidesCardboardButton = _VRModeEnabled; + + [_overlayView setNeedsLayout]; +} + +- (void)startRenderer { + if (!_displayLink) { + // Create a CADisplayLink instance to drive our render loop. + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(display)]; + if ([_displayLink respondsToSelector:@selector(preferredFramesPerSecond)]) { + _displayLink.preferredFramesPerSecond = 60; + } + + [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + _displayLink.paused = _paused; + } + + [self updateOverlayView]; +} + +- (void)stopRenderer { + // Invalidate CADisplayLink instance, which should release us. + [_displayLink invalidate]; + _displayLink = nil; +} + +@end diff --git a/Samples/GVRKit/GVRRendererViewController.h b/Samples/GVRKit/GVRRendererViewController.h new file mode 100644 index 0000000..e903283 --- /dev/null +++ b/Samples/GVRKit/GVRRendererViewController.h @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRRendererView.h" + +/** + * @enum GVRDisplayMode + * + * Enumeration of the display modes, used to identify the |GVRRenderer| used for rendering. + */ +typedef NS_ENUM(NSInteger, GVRDisplayMode) { + kGVRDisplayModeEmbedded, + kGVRDisplayModeFullscreenMonoscopic, + kGVRDisplayModeFullscreenStereoscopic, +}; + +/** Defines a delegate used to provide an instance of |GVRRenderer| to the controller's view. */ +@protocol GVRRendererViewControllerDelegate + +- (GVRRenderer *)rendererForDisplayMode:(GVRDisplayMode)displayMode; + +@optional + +/** Handle user tapping the renderer view. */ +- (void)didTapTriggerButton; + +/** Return YES to hide the transition view. */ +- (BOOL)shouldHideTransitionView; + +@end + +/** + * Defines a view controller to host a |GVRRendererView|. This should be used for presenting + * fullscreen. It also handles interface orientation logic and interfacing with the overlay view + * during viewer pairing. This view controller is also recommended when embedding its view in + * another view controller's view. In that case, this view controller should be added as a child + * view controller to handle |viewWillAppear| and |viewWillDisappear| lifecycle events. + */ +@interface GVRRendererViewController : UIViewController + +/** A convenience initializer to be used with the supplied |GVRRenderer|. */ +- (instancetype)initWithRenderer:(GVRRenderer *)renderer; + +@property(nonatomic, weak) id delegate; + +@property(nonatomic, readonly) GVRRendererView *rendererView; + +@end diff --git a/Samples/GVRKit/GVRRendererViewController.m b/Samples/GVRKit/GVRRendererViewController.m new file mode 100644 index 0000000..af4e0eb --- /dev/null +++ b/Samples/GVRKit/GVRRendererViewController.m @@ -0,0 +1,187 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRRendererViewController.h" + +#import "GVRRendererView.h" + +@interface GVRRendererViewController () { + GVRRenderer *_renderer; +} + +@end + +@implementation GVRRendererViewController + +- (instancetype)initWithRenderer:(GVRRenderer *)renderer { + if (self = [super init]) { + _renderer = renderer; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (GVRRendererView *)rendererView { + return (GVRRendererView *)self.view; +} + +- (void)loadView { + if (!_renderer && [self.delegate respondsToSelector:@selector(rendererForDisplayMode:)]) { + _renderer = [self.delegate rendererForDisplayMode:kGVRDisplayModeEmbedded]; + } + self.view = [[GVRRendererView alloc] initWithRenderer:_renderer]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // We need device orientation change notifications to work. + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + }); + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didChangeOrientation:) + name:UIDeviceOrientationDidChangeNotification + object:nil]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + self.rendererView.paused = NO; + self.rendererView.overlayView.delegate = self; + + if (self.isModal) { + // Back button is always visible when presented. + self.rendererView.overlayView.hidesBackButton = NO; + } +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + self.rendererView.paused = YES; + + // Since our orientation is fixed to landscape right in modal state, upon returning from the modal + // state, reset the UI orientation to the device's orientation. + if (self.isModal) { + [UIViewController attemptRotationToDeviceOrientation]; + } +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + // GVR only supports landscape right orientation when the phone is inserted in the viewer. + if (self.isModal) { + return UIInterfaceOrientationMaskLandscapeRight; + } else { + return [super supportedInterfaceOrientations]; + } +} + +#pragma mark - GVROverlayViewDelegate - Works only when we are presented. + +- (void)didTapTriggerButton { + // In embedded mode, pass the trigger tap to our delegate. + if (self.isModal && [self.delegate respondsToSelector:@selector(didTapTriggerButton)]) { + [self.delegate didTapTriggerButton]; + } +} + +- (void)didTapBackButton { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (UIViewController *)presentingViewControllerForSettingsDialog { + return self; +} + +- (void)didPresentSettingsDialog:(BOOL)presented { + // The overlay view is presenting the settings dialog. Pause our rendering while presented. + self.rendererView.paused = presented; +} + +- (void)didChangeViewerProfile { + // Renderer's OnResume also refreshes viewer profile. + [_renderer refresh]; +} + +- (void)shouldDisableIdleTimer:(BOOL)shouldDisable { + [UIApplication sharedApplication].idleTimerDisabled = shouldDisable; +} + +- (void)didTapCardboardButton { + // Toggle VR mode if we are being presented. + if ([self isModal]) { + self.rendererView.VRModeEnabled = !self.rendererView.VRModeEnabled; + } else { + // We are in embedded mode, present another instance of GVRRendererViewController. + if ([self.delegate respondsToSelector:@selector(rendererForDisplayMode:)]) { + // Check if cardboard or fullscreen button was shown on the overlay view to determine + // VRModeEnabled flag. + BOOL VRModeEnabled = self.rendererView.overlayView.hidesFullscreenButton && + !self.rendererView.overlayView.hidesCardboardButton; + GVRDisplayMode displayMode = (VRModeEnabled ? kGVRDisplayModeFullscreenStereoscopic + : kGVRDisplayModeFullscreenMonoscopic); + GVRRenderer *renderer = [self.delegate rendererForDisplayMode:displayMode]; + + GVRRendererViewController *viewController = + [[GVRRendererViewController alloc] initWithRenderer:renderer]; + GVRRendererView *rendererView = viewController.rendererView; + + if ([self.delegate respondsToSelector:@selector(shouldHideTransitionView)]) { + rendererView.overlayView.hidesTransitionView = [self.delegate shouldHideTransitionView]; + } + + rendererView.VRModeEnabled = VRModeEnabled; + + [self.parentViewController presentViewController:viewController animated:YES completion:nil]; + } + } +} + +#pragma mark - NSNotificationCenter + +- (void)didChangeOrientation:(NSNotification *)notification { + // Request a layout change on the render view since iOS does not call layoutSubviews on 180-degree + // orientation changes. + [self.view setNeedsLayout]; +} + +#pragma mark - Private + +// Returns YES if this view controller is being presented. */ +- (BOOL)isModal { + if ([self presentingViewController]) { + return YES; + } + if ([[[self navigationController] presentingViewController] presentedViewController] == + [self navigationController]) { + return YES; + } + if ([[[self tabBarController] presentingViewController] + isKindOfClass:[UITabBarController class]]) { + return YES; + } + + return NO; +} + +@end diff --git a/Samples/GVRKit/GVRReticleRenderer.h b/Samples/GVRKit/GVRReticleRenderer.h new file mode 100644 index 0000000..6a9970e --- /dev/null +++ b/Samples/GVRKit/GVRReticleRenderer.h @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRSceneRenderer.h" + +/** Defines a renderer for a reticle draw at the center for display. */ +@interface GVRReticleRenderer : NSObject + +/** The depth at which to draw the reticle. */ +@property(nonatomic) CGFloat depth; + +@end diff --git a/Samples/GVRKit/GVRReticleRenderer.mm b/Samples/GVRKit/GVRReticleRenderer.mm new file mode 100644 index 0000000..1690f52 --- /dev/null +++ b/Samples/GVRKit/GVRReticleRenderer.mm @@ -0,0 +1,158 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRReticleRenderer.h" + +// The reticle quad is 2 * SIZE units. +static const float kReticleSize = .01f; +static const float kReticleDistance = 0.3; +static const int kCoordsPerVertex = 3; +static const NSInteger kVertexStrideBytes = kCoordsPerVertex * sizeof(float); +static const float kVertexData[] = { + -kReticleSize, -kReticleSize, -kReticleDistance, + kReticleSize, -kReticleSize, -kReticleDistance, + -kReticleSize, kReticleSize, -kReticleDistance, + kReticleSize, kReticleSize, -kReticleDistance, +}; + +// Vertex shader implementation. +static const char *kVertexShaderString = R"( +uniform mat4 uMvpMatrix; +attribute vec3 aPosition; +varying vec2 vCoords; + +// Passthrough normalized vertex coordinates. +void main() { + gl_Position = uMvpMatrix * vec4(aPosition, 1); + vCoords = aPosition.xy / vec2(.01, .01); +} +)"; + +// Procedurally render a ring on the quad between the specified radii. +static const char *kPassThroughFragmentShaderString = R"( +precision mediump float; +varying vec2 vCoords; + +// Simple ring shader that is white between the radii and transparent elsewhere. +void main() { + float r = length(vCoords); + // Blend the edges of the ring at .55 +/- .05 and .85 +/- .05. + float alpha = smoothstep(0.5, 0.6, r) * (1.0 - smoothstep(0.8, 0.9, r)); + if (alpha == 0.0) { + discard; + } else { + gl_FragColor = vec4(alpha); + } +} +)"; + +@implementation GVRReticleRenderer { + GLuint _program; + GLint _positionAttrib; + GLint _mvpMatrix; + GLuint _vertex_buffer; +} + +- (instancetype)init { + if (self = [super init]) { + _depth = -0.3; + } + return self; +} + +@synthesize initialized; +@synthesize hidden; + +- (void)initializeGl { + // Load the vertex/fragment shaders. + const GLuint vertex_shader = loadShader(GL_VERTEX_SHADER, kVertexShaderString); + NSAssert(vertex_shader != 0, @"Failed to load vertex shader"); + const GLuint fragment_shader = + loadShader(GL_FRAGMENT_SHADER, kPassThroughFragmentShaderString); + NSAssert(fragment_shader != 0, @"Failed to load fragment shader"); + + _program = glCreateProgram(); + NSAssert(_program != 0, @"Failed to create program"); + glAttachShader(_program, vertex_shader); + glAttachShader(_program, fragment_shader); + + // Link the shader program. + glLinkProgram(_program); + NSAssert(checkProgramLinkStatus(_program), @"Failed to link _program"); + + // Get the location of our attributes so we can bind data to them later. + _positionAttrib = glGetAttribLocation(_program, "aPosition"); + NSAssert(_positionAttrib != -1, @"glGetAttribLocation failed for aPosition"); + _mvpMatrix = glGetUniformLocation(_program, "uMvpMatrix"); + NSAssert(_mvpMatrix != -1, @"Error fetching uniform values for shader."); + + glGenBuffers(1, &_vertex_buffer); + NSAssert(_vertex_buffer != 0, @"glGenBuffers failed for vertex buffer"); + glBindBuffer(GL_ARRAY_BUFFER, _vertex_buffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(kVertexData), kVertexData, GL_STATIC_DRAW); + + checkGLError("initialize"); + + initialized = YES; +} + +- (void)clearGl { + if (_vertex_buffer) { + GLuint buffers[] = {_vertex_buffer}; + glDeleteBuffers(1, buffers); + } + if (_program) { + glDeleteProgram(_program); + } + initialized = NO; +} + +- (void)update:(GVRHeadPose *)headPose { +} + +- (void)draw:(GVRHeadPose *)headPose { + glDisable(GL_DEPTH_TEST); + glClear(GL_DEPTH_BUFFER_BIT); + checkGLError("glClear"); + + // Configure shader. + glUseProgram(_program); + checkGLError("program"); + + GLKMatrix4 modelViewMatrix = headPose.eyeTransform; + modelViewMatrix = GLKMatrix4Translate(modelViewMatrix, 0, 0, _depth); + GLKMatrix4 projection_matrix = [headPose projectionMatrixWithNear:0.1f far:100.0f]; + modelViewMatrix = GLKMatrix4Multiply(projection_matrix, modelViewMatrix); + + glUniformMatrix4fv(_mvpMatrix, 1, GL_FALSE, modelViewMatrix.m); + checkGLError("mvpMatrix"); + + // Render quad. + glBindBuffer(GL_ARRAY_BUFFER, _vertex_buffer); + glEnableVertexAttribArray(_positionAttrib); + glVertexAttribPointer( + _positionAttrib, kCoordsPerVertex, GL_FLOAT, GL_FALSE, kVertexStrideBytes, 0); + + checkGLError("vertex data"); + + int numVertices = sizeof(kVertexData) / sizeof(float) / kCoordsPerVertex; + glDrawArrays(GL_TRIANGLE_STRIP, 0, numVertices); + checkGLError("glDrawArrays"); + + glDisableVertexAttribArray(_positionAttrib); +} + +@end diff --git a/Samples/GVRKit/GVRSceneRenderer.h b/Samples/GVRKit/GVRSceneRenderer.h new file mode 100644 index 0000000..035f728 --- /dev/null +++ b/Samples/GVRKit/GVRSceneRenderer.h @@ -0,0 +1,85 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRRenderer.h" + +/** + * Defines an interface for a render object that is placed in a scene graph. + */ +@protocol GVRRenderObject + +/** + * Returns YES if the render object is initialized. This is useful when the + * object is moved around in the scene graph. + */ +@property(nonatomic, readonly) BOOL initialized; + +/** Allows hiding or showing the render object in the scene. */ +@property(nonatomic) BOOL hidden; + +/** Initializes GL resources needed for rendering the render object. */ +- (void)initializeGl; + +/** Renders the object given the head pose. */ +- (void)draw:(GVRHeadPose *)headPose; + +@optional +/** Releases the GL resources. */ +- (void)clearGl; + +/** Updates GL state needed before ::draw: is called. */ +- (void)update:(GVRHeadPose *)headPose; + +/** Pauses or resumes rendering. */ +- (void)pause:(BOOL)pause; + +/** + * Returns YES if render object handles the trigger button press. This stops + * render objects next in the display list from handling the trigger press. + */ +- (BOOL)handleTrigger:(GVRHeadPose *)headPose; +@end + + +/** + * Defines a simple list of render objects. Each item in the list implements the + * |GVRRenderObject| protocol, including GVRRenderList itself. This results in + * a scene graph data structure. Objects in the list are drawn in depth-first order. + */ +@interface GVRRenderList : NSObject +- (void)addRenderObject:(id)renderTarget; +- (void)insertRenderObject:(id)renderTarget atIndex:(NSUInteger)index; +- (void)removeRenderObject:(id)renderTarget; +- (void)removeRenderObjectAtIndex:(NSUInteger)index; +- (void)removeAll; +- (id)objectAtIndex:(NSUInteger)index; +- (NSUInteger)count; +@end + +/** Defines a scene renderer with a render list of |GVRRenderObject| objects. */ +@interface GVRSceneRenderer : GVRRenderer + +@property(nonatomic) GVRRenderList *renderList; + +@property(nonatomic) BOOL hidesReticle; + +@end + +// Helper methods used during OpenGL rendering. +void checkGLError(const char *label); +GLuint loadShader(GLenum type, const char *shader_src); +bool checkProgramLinkStatus(GLuint shader_program); + diff --git a/Samples/GVRKit/GVRSceneRenderer.mm b/Samples/GVRKit/GVRSceneRenderer.mm new file mode 100644 index 0000000..1d293bd --- /dev/null +++ b/Samples/GVRKit/GVRSceneRenderer.mm @@ -0,0 +1,253 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRSceneRenderer.h" + +#import "GVRReticleRenderer.h" + +@implementation GVRSceneRenderer { + GVRReticleRenderer *_reticle; +} + +#pragma mark - GVRRenderer + +- (instancetype)init { + if (self = [super init]) { + _renderList = [[GVRRenderList alloc] init]; + _reticle = [[GVRReticleRenderer alloc] init]; + } + return self; +} + +- (void)initializeGl { + [super initializeGl]; + + [_renderList initializeGl]; + [_reticle initializeGl]; +} + +- (void)clearGl { + [super clearGl]; + + [_reticle clearGl]; + [_renderList clearGl]; +} + +- (void)update:(GVRHeadPose *)headPose { + checkGLError("pre update"); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glEnable(GL_DEPTH_TEST); + glEnable(GL_SCISSOR_TEST); + checkGLError("update"); + + [_renderList update:headPose]; + + if (!self.hidesReticle) { + [_reticle update:headPose]; + } +} + +- (void)draw:(GVRHeadPose *)headPose { + CGRect viewport = [headPose viewport]; + glViewport(viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height); + glScissor(viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + checkGLError("glClear"); + + [_renderList draw:headPose]; + + if (!self.hidesReticle) { + [_reticle draw:headPose]; + } +} + +- (void)pause:(BOOL)pause { + [super pause:pause]; + + [_renderList pause:pause]; +} + +- (BOOL)handleTrigger:(GVRHeadPose *)headPose { + return [_renderList handleTrigger:headPose]; +} + +@end + +#pragma mark - GVRRenderList + +@implementation GVRRenderList { + NSMutableArray *_renderObjects; +} + +@synthesize initialized; +@synthesize hidden; + +- (instancetype)init { + if (self = [super init]) { + _renderObjects = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addRenderObject:(id)renderTarget { + [_renderObjects addObject:renderTarget]; +} + +- (void)insertRenderObject:(id)renderTarget atIndex:(NSUInteger)index { + [_renderObjects insertObject:renderTarget atIndex:index]; +} + +- (void)removeRenderObject:(id)renderTarget { + [_renderObjects removeObject:renderTarget]; +} + +- (void)removeRenderObjectAtIndex:(NSUInteger)index { + [_renderObjects removeObjectAtIndex:index]; +} + +- (void)removeAll { + [_renderObjects removeAllObjects]; +} + +- (id)objectAtIndex:(NSUInteger)index { + return [_renderObjects objectAtIndex:index]; +} + +- (NSUInteger)count { + return _renderObjects.count; +} + +#pragma mark - GVRRenderObject + +- (void)initializeGl { + for (idtarget in _renderObjects) { + [target initializeGl]; + } + initialized = YES; +} + +- (void)clearGl { + for (idtarget in _renderObjects) { + if ([target respondsToSelector:@selector(clearGl)]) { + [target clearGl]; + } + } + initialized = NO; +} + +- (void)update:(GVRHeadPose *)headPose { + for (idtarget in _renderObjects) { + if (!target.initialized) { + [target initializeGl]; + } + if (!target.hidden && target.initialized && [target respondsToSelector:@selector(update:)]) { + [target update:headPose]; + } + } +} + +- (void)draw:(GVRHeadPose *)headPose { + for (idtarget in _renderObjects) { + if (!target.hidden && target.initialized) { + [target draw:headPose]; + } + } +} + +- (void)pause:(BOOL)pause { + for (idtarget in _renderObjects) { + if ([target respondsToSelector:@selector(pause:)]) { + [target pause:pause]; + } + } +} + +- (BOOL)handleTrigger:(GVRHeadPose *)headPose { + for (idtarget in _renderObjects) { + if ([target respondsToSelector:@selector(handleTrigger:)]) { + if ([target handleTrigger:headPose]) { + return YES; + } + } + } + return NO; +} + +@end + +#pragma mark - GL Helper methods + +void checkGLError(const char *label) { + int gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + NSLog(@"GL error %s: %d", label, gl_error); + } + assert(glGetError() == GL_NO_ERROR); +} + +GLuint loadShader(GLenum type, const char *shader_src) { + GLint compiled = 0; + + // Create the shader object + const GLuint shader = glCreateShader(type); + if (shader == 0) { + return 0; + } + // Load the shader source + glShaderSource(shader, 1, &shader_src, NULL); + + // Compile the shader + glCompileShader(shader); + // Check the compile status + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + + if (!compiled) { + GLint info_len = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &info_len); + + if (info_len > 1) { + char *info_log = ((char *)malloc(sizeof(char) * info_len)); + glGetShaderInfoLog(shader, info_len, NULL, info_log); + NSLog(@"Error compiling shader:%s", info_log); + free(info_log); + } + glDeleteShader(shader); + return 0; + } + return shader; +} + +// Checks the link status of the given program. +bool checkProgramLinkStatus(GLuint shader_program) { + GLint linked = 0; + glGetProgramiv(shader_program, GL_LINK_STATUS, &linked); + + if (!linked) { + GLint info_len = 0; + glGetProgramiv(shader_program, GL_INFO_LOG_LENGTH, &info_len); + + if (info_len > 1) { + char *info_log = ((char *)malloc(sizeof(char) * info_len)); + glGetProgramInfoLog(shader_program, info_len, NULL, info_log); + NSLog(@"Error linking program: %s", info_log); + free(info_log); + } + glDeleteProgram(shader_program); + return false; + } + return true; +} diff --git a/Samples/GVRKit/GVRTextureRenderer.h b/Samples/GVRKit/GVRTextureRenderer.h new file mode 100644 index 0000000..2450706 --- /dev/null +++ b/Samples/GVRKit/GVRTextureRenderer.h @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRSceneRenderer.h" + +/** Defines mesh type needed for rendering stereoscopic content. */ +typedef NS_ENUM(NSInteger, GVRMeshType) { + kGVRMeshTypeStereoLeftRight = 0, + kGVRMeshTypeStereoTopBottom, + kGVRMeshTypeMonoscopic +}; + +/** + * Defines a renderer for OpenGL textures. It supports rendering individual + * image textures and dual (Y + UV) video textures. These textures can be + * mapped to spherical or quad meshes. + */ +@interface GVRTextureRenderer : NSObject + +/** The position transform, in world space, where the texture will be placed. */ +@property(nonatomic) GLKMatrix4 position; + +/** The minimum bounding box vertex position. */ +@property(nonatomic, readonly) GLKVector3 aabbMin; + +/** The maximum bounding box vertex position. */ +@property(nonatomic, readonly) GLKVector3 aabbMax; + +/** + * Set spherical mesh for the texture. The mesh does not have be closed. + * + * @param radius Size of the sphere in meters. Must be > 0. + * @param latitudes Number of rows that make up the sphere. Must be >= 1. + * @param longitudes Number of columns that make up the sphere. Must be >= 1. + * @param verticalFov Total latitudinal degrees that are covered by the sphere. + * Must be in (0, 180]. + * @param horizontalFov Total longitudinal degrees that are covered by the + * sphere.Must be in (0, 360]. + * @param meshType The mesh type to use depending upon the media format. + */ +- (void)setSphericalMeshOfRadius:(CGFloat)radius + latitudes:(NSInteger)latitudes + longitudes:(NSInteger)longitudes + verticalFov:(NSInteger)verticalFov + horizontalFov:(NSInteger)horizontalFov + meshType:(GVRMeshType)meshType; + +/** + * Set a quad mesh with the given width and height. The unit for width and + * height is specified in meters. + * + * @param width The width in meters. + * @param height The height in meters. + * @param meshType The mesh type to use depending upon the media format. + * + * Use this to compute meters from pixels: + * 1 meter width at 1 meter depth = 2 * atan(0.5) = 53.13 degrees per meter. + * 15 pixels/degree * 53.13 degrees/meter = 796.951535313 pixels/meter. + */ +- (void)setQuadMeshOfWidth:(CGFloat)width + height:(CGFloat)height + meshType:(GVRMeshType)meshType; + +/** Sets the texture id for an image. See |GVRImageRenderer|. */ +- (void)setImageTextureId:(GLuint)textureId; + +/** + * Sets the textures for Y and UV of an individual video frame, along with the + * color conversion matrix. See |GVRVideoRenderer|. + */ +- (void)setVideoYTextureId:(GLuint)yTextureId + uvTextureId:(GLuint)uvTextureId + colorConversionMatrix:(GLKMatrix4)colorConversionMatrix; + +@end diff --git a/Samples/GVRKit/GVRTextureRenderer.mm b/Samples/GVRKit/GVRTextureRenderer.mm new file mode 100644 index 0000000..fe12d65 --- /dev/null +++ b/Samples/GVRKit/GVRTextureRenderer.mm @@ -0,0 +1,427 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRTextureRenderer.h" + +// Constants related to vertex data. +static const NSInteger POSITION_COORDS_PER_VERTEX = 3; +// The vertex contains texture coordinates for both the left & right eyes. If the SCENE is +// rendered in VR, the appropriate part of the vertex would be selected at runtime. For mono +// SCENES, only the left eye's UV coordinates are used. +// For mono MEDIA, the UV coordinates are duplicated in each. For stereo MEDIA, the UV coords +// poNSInteger to the appropriate part of the source media. +static const NSInteger TEXTURE_COORDS_PER_VERTEX = 2 * 2; +static const NSInteger COORDS_PER_VERTEX = POSITION_COORDS_PER_VERTEX + TEXTURE_COORDS_PER_VERTEX; +static const NSInteger VERTEX_STRIDE_BYTES = COORDS_PER_VERTEX * sizeof(float); + +// Vertex shader implementation. +static const char *kVertexShaderString = R"( + #version 100 + + uniform mat4 uMvpMatrix; + attribute vec4 aPosition; + attribute vec2 aTexCoords; + varying vec2 vTexCoords; + void main(void) { + gl_Position = uMvpMatrix * aPosition; + vTexCoords = aTexCoords; + } +)"; + +// Simple pass-through texture fragment shader. +static const char *kPassThroughFragmentShaderString = R"( + + #ifdef GL_ES + precision mediump float; + #endif + uniform sampler2D uTexture; + varying vec2 vTexCoords; + + void main(void) { + gl_FragColor = texture2D(uTexture, vTexCoords); + } +)"; + +// Simple pass-through video fragment shader. +static const char *kPassThroughVideoFragmentShaderString = R"( + + #ifdef GL_ES + precision mediump float; + #endif + uniform sampler2D uTextureY; + uniform sampler2D uTextureUV; + uniform mat4 uColorConversionMatrix; + varying vec2 vTexCoords; + + void main(void) { + float u = vTexCoords.x; + float v = vTexCoords.y; + + vec3 yuv; + yuv.x = texture2D(uTextureY, vec2(u, v)).r; + yuv.yz = texture2D(uTextureUV, vec2(u, v)).rg; + gl_FragColor = uColorConversionMatrix * vec4(yuv, 1.0); + } +)"; + +@implementation GVRTextureRenderer { + BOOL _initialized; + BOOL _hidden; + BOOL _hasTextureId; + BOOL _isVideoTextureRenderer; + BOOL _flipTextureVertically; + BOOL _needsRebindVertexData; + NSData *_vertexData; + GLuint _textureId; + GLuint _yTextureId; + GLuint _uvTextureId; + GLKMatrix4 _colorConversionMatrix; + + GLuint _program; + GLint _positionAttrib; + GLint _texCoords; + GLint _mvpMatrix; + GLint _texture; + GLint _yTexture; + GLint _uvTexture; + GLint _colorMatrix; + GLuint _vertex_buffer; +} + +@synthesize initialized; +@synthesize hidden; + +- (instancetype)init { + if (self = [super init]) { + _position = GLKMatrix4Identity; + } + return self; +} + +- (void)setSphericalMeshOfRadius:(CGFloat)radius + latitudes:(NSInteger)latitudes + longitudes:(NSInteger)longitudes + verticalFov:(NSInteger)verticalFov + horizontalFov:(NSInteger)horizontalFov + meshType:(GVRMeshType)meshType { + _vertexData = [GVRTextureRenderer makeSphereWithRadius:radius + latitudes:latitudes + longitudes:longitudes + verticalFov:verticalFov + horizontalFov:horizontalFov + meshType:meshType]; + [self computeAabbFromVertexData:_vertexData]; + _needsRebindVertexData = YES; +} + +- (void)setQuadMeshOfWidth:(CGFloat)width height:(CGFloat)height meshType:(GVRMeshType)meshType { + _vertexData = [GVRTextureRenderer makeQuadWithWidth:width height:height meshType:meshType]; + [self computeAabbFromVertexData:_vertexData]; + _needsRebindVertexData = YES; +} + +- (void)setIsVideoTextureRenderer:(BOOL)isVideoTextureRenderer { + _isVideoTextureRenderer = YES; +} + +- (void)setFlipTextureVertically:(BOOL)flipTextureVertically { + _flipTextureVertically = flipTextureVertically; +} + +- (void)setImageTextureId:(GLuint)textureId { + _hasTextureId = YES; + _textureId = textureId; +} + +- (void)setVideoYTextureId:(GLuint)yTextureId + uvTextureId:(GLuint)uvTextureId + colorConversionMatrix:(GLKMatrix4)colorConversionMatrix { + _hasTextureId = YES; + _yTextureId = yTextureId; + _uvTextureId = uvTextureId; + _colorConversionMatrix = colorConversionMatrix; +} + +- (void)initializeGl { + // Load the vertex/fragment shaders. + const GLuint vertex_shader = loadShader(GL_VERTEX_SHADER, kVertexShaderString); + NSAssert(vertex_shader != 0, @"Failed to load vertex shader"); + const GLuint fragment_shader = + loadShader(GL_FRAGMENT_SHADER, + _isVideoTextureRenderer ? kPassThroughVideoFragmentShaderString + : kPassThroughFragmentShaderString); + NSAssert(fragment_shader != 0, @"Failed to load fragment shader"); + + _program = glCreateProgram(); + NSAssert(_program != 0, @"Failed to create program"); + glAttachShader(_program, vertex_shader); + glAttachShader(_program, fragment_shader); + + // Link the shader program. + glLinkProgram(_program); + NSAssert(checkProgramLinkStatus(_program), @"Failed to link _program"); + + // Get the location of our attributes so we can bind data to them later. + _positionAttrib = glGetAttribLocation(_program, "aPosition"); + NSAssert(_positionAttrib != -1, @"glGetAttribLocation failed for aPosition"); + _texCoords = glGetAttribLocation(_program, "aTexCoords"); + NSAssert(_texCoords != -1, @"glGetAttribLocation failed for aTexCoords"); + _mvpMatrix = glGetUniformLocation(_program, "uMvpMatrix"); + if (_isVideoTextureRenderer) { + _yTexture = glGetUniformLocation(_program, "uTextureY"); + _uvTexture = glGetUniformLocation(_program, "uTextureUV"); + _colorMatrix = glGetUniformLocation(_program, "uColorConversionMatrix"); + NSAssert(_mvpMatrix != -1 && _yTexture != -1 && _uvTexture != -1 && _colorMatrix != -1, + @"Error fetching uniform values for shader."); + } else { + _texture = glGetUniformLocation(_program, "uTexture"); + NSAssert(_mvpMatrix != -1 && _texture != -1, @"Error fetching uniform values for shader."); + } + + checkGLError("initialize"); + + initialized = YES; +} + +- (void)clearGl { + if (_vertex_buffer) { + GLuint buffers[] = {_vertex_buffer}; + glDeleteBuffers(1, buffers); + _vertex_buffer = 0; + } + if (_program) { + glDeleteProgram(_program); + _program = 0; + } + if (_hasTextureId) { + _hasTextureId = NO; + if (_isVideoTextureRenderer) { + GLuint textures[] = {_yTextureId, _uvTextureId}; + glDeleteTextures(2, textures); + } else { + GLuint textures[] = {_textureId}; + glDeleteTextures(1, textures); + } + } + initialized = NO; +} + +- (void)update:(GVRHeadPose *)headPose { + if (_needsRebindVertexData) { + _needsRebindVertexData = NO; + + if (_vertex_buffer) { + GLuint buffers[] = {_vertex_buffer}; + glDeleteBuffers(1, buffers); + _vertex_buffer = 0; + } + } + if (_vertex_buffer == 0) { + glGenBuffers(1, &_vertex_buffer); + NSAssert(_vertex_buffer != 0, @"glGenBuffers failed for vertex buffer"); + glBindBuffer(GL_ARRAY_BUFFER, _vertex_buffer); + glBufferData(GL_ARRAY_BUFFER, _vertexData.length, _vertexData.bytes, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } +} + +- (void)draw:(GVRHeadPose *)headPose { + if (!_hasTextureId) { + return; + } + + GLKMatrix4 modelMatrix = _position; + if (_flipTextureVertically) { + modelMatrix = GLKMatrix4Scale(modelMatrix, 1, -1, 1); + } + GLKMatrix4 viewMatrix = headPose.viewTransform; + GLKMatrix4 projectionMatrix = [headPose projectionMatrixWithNear:0.1f far:100.0f]; + + GLKMatrix4 modelViewProjectionMatrix = GLKMatrix4Multiply(projectionMatrix, viewMatrix); + modelViewProjectionMatrix = GLKMatrix4Multiply(modelViewProjectionMatrix, modelMatrix); + + // Select our shader. + glUseProgram(_program); + checkGLError("glUseProgram"); + + glUniformMatrix4fv(_mvpMatrix, 1, GL_FALSE, modelViewProjectionMatrix.m); + if (_isVideoTextureRenderer) { + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _yTextureId); + glUniform1i(_yTexture, 0); + + checkGLError("bind video texture 0"); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, _uvTextureId); + glUniform1i(_uvTexture, 1); + + checkGLError("bind video texture 1"); + + glUniformMatrix4fv(_colorMatrix, 1, GL_FALSE, _colorConversionMatrix.m); + + checkGLError("color matrix"); + } else { + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _textureId); + glUniform1i(_texture, 0); + checkGLError("bind texture"); + } + + glEnableVertexAttribArray(_positionAttrib); + glEnableVertexAttribArray(_texCoords); + checkGLError("enable vertex attribs"); + + // Load position data. + glBindBuffer(GL_ARRAY_BUFFER, _vertex_buffer); + glVertexAttribPointer( + _positionAttrib, POSITION_COORDS_PER_VERTEX, GL_FLOAT, GL_FALSE, VERTEX_STRIDE_BYTES, 0); + checkGLError("vertex position"); + + // Load texture data. + int textureOffset = + (headPose.eye == kGVRRightEye) ? POSITION_COORDS_PER_VERTEX + 2 : POSITION_COORDS_PER_VERTEX; + glVertexAttribPointer(_texCoords, + TEXTURE_COORDS_PER_VERTEX, + GL_FLOAT, + GL_FALSE, + VERTEX_STRIDE_BYTES, + (void *)(textureOffset * sizeof(float))); + checkGLError("texture offset"); + + // Render. + GLsizei numVertices = (GLsizei)(_vertexData.length / sizeof(float)) / COORDS_PER_VERTEX; + glDrawArrays(GL_TRIANGLE_STRIP, 0, numVertices); + checkGLError("glDrawArrays"); + + // Clear state. + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDisableVertexAttribArray(_positionAttrib); + glDisableVertexAttribArray(_texCoords); + checkGLError("done drawing"); +} + +#pragma mark - Private + +- (void)computeAabbFromVertexData:(NSData *)vertexData { + for (int i = 0; i < vertexData.length; i = i + (sizeof(float) * COORDS_PER_VERTEX)) { + float *pos = (float *)vertexData.bytes + i / sizeof(float); + GLKVector3 v = GLKVector3Make(*(pos), *(pos + 1), *(pos + 2)); + _aabbMin = GLKVector3Minimum(_aabbMin, v); + _aabbMax = GLKVector3Maximum(_aabbMax, v); + } +} + ++ (NSData *)makeSphereWithRadius:(CGFloat)radius + latitudes:(NSInteger)latitudes + longitudes:(NSInteger)longitudes + verticalFov:(NSInteger)verticalFov + horizontalFov:(NSInteger)horizontalFov + meshType:(GVRMeshType)meshType { + // Compute angular size of each UV quad. + float verticalFovRads = GLKMathDegreesToRadians(verticalFov); + float horizontalFovRads = GLKMathDegreesToRadians(horizontalFov); + float quadHeightRads = verticalFovRads / latitudes; + float quadWidthRads = horizontalFovRads / longitudes; + NSInteger vertexCount = (2 * (longitudes + 1) + 2) * latitudes; + + NSInteger CPV = COORDS_PER_VERTEX; + float vertexData[vertexCount * CPV]; + + // Generate the data for the sphere which is a set of triangle strips representing each + // latitude band. + NSInteger v = 0; // Index into the vertex array. + // (i, j) represents a quad in the equirectangular sphere. + for (NSInteger j = 0; j < latitudes; ++j) { + // Each latitude band lies between the two phi values. Each vertical edge on a band lies on + // a theta value. + float phiLow = (quadHeightRads * j - verticalFovRads / 2.0f); + float phiHigh = (quadHeightRads * (j + 1) - verticalFovRads / 2.0f); + for (NSInteger i = 0; i < longitudes + 1; ++i) { // For each vertical edge in the band. + for (NSInteger k = 0; k < 2; ++k) { // For low and high points on an edge. + // For each point, determine it's angular position. + float phi = (k == 0) ? phiLow : phiHigh; + float theta = quadWidthRads * i + (float)M_PI - horizontalFovRads / 2.0f; + + // Set vertex position data. + vertexData[CPV * v + 0] = -(float)(radius * sin(theta) * cos(phi)); + vertexData[CPV * v + 1] = (float)(radius * sin(phi)); + vertexData[CPV * v + 2] = (float)(radius * cos(theta) * cos(phi)); + + // Set vertex texture.x data. + if (meshType == kGVRMeshTypeStereoLeftRight) { + vertexData[CPV * v + 3] = (i * quadWidthRads / horizontalFovRads) / 2.0f; + vertexData[CPV * v + 5] = (i * quadWidthRads / horizontalFovRads) / 2.0f + .5f; + } else { + vertexData[CPV * v + 3] = i * quadWidthRads / horizontalFovRads; + vertexData[CPV * v + 5] = i * quadWidthRads / horizontalFovRads; + } + + // Set vertex texture.y data. The "1 - ..." is due to Canvas vs GL coords. + if (meshType == kGVRMeshTypeStereoTopBottom) { + vertexData[CPV * v + 4] = 1 - (((j + k) * quadHeightRads / verticalFovRads) / 2.0f + .5f); + vertexData[CPV * v + 6] = 1 - ((j + k) * quadHeightRads / verticalFovRads) / 2.0f; + } else { + vertexData[CPV * v + 4] = 1 - (j + k) * quadHeightRads / verticalFovRads; + vertexData[CPV * v + 6] = 1 - (j + k) * quadHeightRads / verticalFovRads; + } + v++; + + // Break up the triangle strip using degenerate vertices by copying first and last points. + if ((i == 0 && k == 0) || (i == longitudes && k == 1)) { + // System.arraycopy(vertexData, CPV * (v - 1), vertexData, CPV * v, CPV); + memcpy(vertexData + (CPV * v), vertexData + (CPV * (v - 1)), CPV * sizeof(float)); + v++; + } + } + // Move on to the next vertical edge in the triangle strip. + } + // Move on to the next triangle strip. + } + + return [NSData dataWithBytes:vertexData length:sizeof(vertexData)]; +} + ++ (NSData *)makeQuadWithWidth:(CGFloat)width height:(CGFloat)height meshType:(GVRMeshType)meshType { + NSData *data = nil; + float w = (float)width; + float h = (float)height; + + switch (meshType) { + case kGVRMeshTypeStereoLeftRight: { + float vertices[] = {-w / 2, -h / 2, 0, 0, 1, .5f, 1, w, -h / 2, 0, .5f, 1, 1, 1, + -w / 2, h, 0, 0, 0, .5f, 0, w, h, 0, .5f, 0, 1, 0}; + data = [NSData dataWithBytes:vertices length:sizeof(vertices)]; + } break; + + case kGVRMeshTypeStereoTopBottom: { + float vertices[] = {-w / 2, -h / 2, 0, 0, .5f, 0, 1, w, -h / 2, 0, 1, .5f, 1, 1, + -w / 2, h, 0, 0, 0, 0, .5f, w, h, 0, 1, 0, 1, .5f}; + data = [NSData dataWithBytes:vertices length:sizeof(vertices)]; + } break; + + case kGVRMeshTypeMonoscopic: + default: { + float vertices[] = {-w / 2, -h / 2, 0, 0, 1, 0, 1, w, -h / 2, 0, 1, 1, 1, 1, + -w / 2, h, 0, 0, 0, 0, 0, w, h, 0, 1, 0, 1, 0}; + data = [NSData dataWithBytes:vertices length:sizeof(vertices)]; + } break; + } + return data; +} + +@end diff --git a/Samples/GVRKit/GVRUIViewRenderer.h b/Samples/GVRKit/GVRUIViewRenderer.h new file mode 100644 index 0000000..c49cc54 --- /dev/null +++ b/Samples/GVRKit/GVRUIViewRenderer.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRTextureRenderer.h" + +/** Defines a texture renderer for UIView renderer to pixel buffers. */ +@interface GVRUIViewRenderer : GVRTextureRenderer + +/** Initializes with the given view. */ +- (instancetype)initWithView:(UIView *)view; + +@property(nonatomic) UIView *view; + +@end diff --git a/Samples/GVRKit/GVRUIViewRenderer.mm b/Samples/GVRKit/GVRUIViewRenderer.mm new file mode 100644 index 0000000..5bbd71f --- /dev/null +++ b/Samples/GVRKit/GVRUIViewRenderer.mm @@ -0,0 +1,250 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRUIViewRenderer.h" + +#include + +// 1 meter width at 1 meter depth = 2 * atan(0.5) = 53.13 degrees per meter. +// 15 pixels per degree * 53.13 degrees per meter = 796.951535313 pixels per meter. +static const CGFloat kPixelsPerMeter = 796.951535313f; +static constexpr float kDefaultEpsilon = 1.0e-5f; + +@interface GVRTextureRenderer (Subclassing) +- (void)setFlipTextureVertically:(BOOL)flipTextureVertically; +@end + +@implementation GVRUIViewRenderer { + UIView *_view; + CVPixelBufferRef _pixelBuffer; + CVOpenGLESTextureRef _texture; + CVOpenGLESTextureCacheRef _textureCache; + CGPoint _hitTestPoint; +} + +- (instancetype)initWithView:(UIView *)view { + if (self = [super init]) { + super.flipTextureVertically = YES; + + _view = view; + } + return self; +} + +- (void)dealloc { + [self cleanUpTextures]; + [self clearGl]; +} + +- (void)setView:(UIView *)view { + [self willChangeValueForKey:@"view"]; + _view = view; + [self didChangeValueForKey:@"view"]; +} + +#pragma mark - GVRTextureRenderer + +- (void)initializeGl { + [super initializeGl]; + + if (self.initialized) { + // Create texture cache. + CVReturn status = CVOpenGLESTextureCacheCreate( + kCFAllocatorDefault, NULL, [EAGLContext currentContext], NULL, &_textureCache); + NSAssert(status == noErr, @"Error at CVOpenGLESTextureCacheCreate %d", status); + } +} + +- (void)clearGl { + [super clearGl]; + + if (_pixelBuffer) { + CVPixelBufferRelease(_pixelBuffer); + _pixelBuffer = NULL; + } + if (_textureCache) { + CFRelease(_textureCache); + _textureCache = NULL; + } +} + +- (void)update:(GVRHeadPose *)headPose { + // We should have a non-empty view to render. + if (!_view || CGRectIsEmpty(_view.bounds)) { + // Cleanup previous textures. + [self cleanUpTextures]; + return; + } + + // Create a pixel buffer to render the UIView if it does not exist or view's size has changed. + if (!_pixelBuffer || CVPixelBufferGetWidth(_pixelBuffer) != _view.bounds.size.width || + CVPixelBufferGetHeight(_pixelBuffer) != _view.bounds.size.height) { + // De-allocate previous pixel buffer. + if (_pixelBuffer) { + CVPixelBufferRelease(_pixelBuffer); + } + + // Set the mesh from the view size. + [self setMeshFromSize:_view.bounds.size]; + + // Now create the pixel buffer. + NSDictionary *options = @{ + (id)kCVPixelBufferIOSurfacePropertiesKey : @{}, + (id)kCVPixelBufferOpenGLCompatibilityKey : @(YES), + (id)kCVPixelBufferOpenGLESTextureCacheCompatibilityKey : @(YES) + }; + CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, + (int)_view.bounds.size.width, + (int)_view.bounds.size.height, + kCVPixelFormatType_32BGRA, + (__bridge CFDictionaryRef)options, + &_pixelBuffer); + NSAssert(status == kCVReturnSuccess, @"Error allocating pixel buffer %d", status); + } + + CVReturn status = CVPixelBufferLockBaseAddress(_pixelBuffer, 0); + NSAssert(status == kCVReturnSuccess, @"Error locking pixel buffer %d", status); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + NSAssert(colorSpace != NULL, @"Error creating color space"); + + CGContextRef context = CGBitmapContextCreate(CVPixelBufferGetBaseAddress(_pixelBuffer), + CVPixelBufferGetWidth(_pixelBuffer), + CVPixelBufferGetHeight(_pixelBuffer), + 8, + CVPixelBufferGetBytesPerRow(_pixelBuffer), + colorSpace, + kCGImageAlphaPremultipliedLast); + CGColorSpaceRelease(colorSpace); + NSAssert(context != NULL, @"Error creating bitmap context."); + + // Draw the view to the pixel buffer. + [_view.layer renderInContext:context]; + + CGContextRelease(context); + CVPixelBufferUnlockBaseAddress(_pixelBuffer, 0); + + // Cleanup previous textures. + [self cleanUpTextures]; + + // Create a texture from the pixel buffer. + status = CVOpenGLESTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, // The CFAllocatorRef to use for allocating the texture object. + _textureCache, // The texture cache object that will manage the texture. + _pixelBuffer, // The CVImageBufferRef that you want to create a texture from. + NULL, // A CFDictionaryRef for creating the CVOpenGLESTextureRef objects. + GL_TEXTURE_2D, // The target texture. Can be GL_TEXTURE_2D or GL_RENDERBUFFER. + GL_RGBA, // The number of color components in the texture. + (GLsizei)CVPixelBufferGetWidth(_pixelBuffer), // The width of the texture image. + (GLsizei)CVPixelBufferGetHeight(_pixelBuffer), // The height of the texture image. + GL_RGBA, // The format of the pixel data. + GL_UNSIGNED_BYTE, // The data type of the pixel data. + 0, // The plane of the CVImageBufferRef to map bind. + &_texture); // Where the newly created texture object will be placed. + + if (status == kCVReturnSuccess) { + [self setImageTextureId:CVOpenGLESTextureGetName(_texture)]; + } + + // Perform hittest for hover animations. + // if (_view.userInteractionEnabled) { + // [self hitTest:headPose]; + //} + + [super update:headPose]; +} + +- (BOOL)handleTrigger:(GVRHeadPose *)headPose { + if (_view.userInteractionEnabled && [self hitTest:headPose]) { + UIView *subview = [_view hitTest:_hitTestPoint withEvent:nil]; + if ([subview respondsToSelector:@selector(sendActionsForControlEvents:)]) { + [(UIControl *)subview sendActionsForControlEvents:UIControlEventTouchUpInside]; + return YES; + } + } + return NO; +} + +#pragma mark - Private + +- (void)setMeshFromSize:(CGSize)size { + CGFloat width = size.width / kPixelsPerMeter; + CGFloat height = size.height / kPixelsPerMeter; + + [self setQuadMeshOfWidth:width height:height meshType:kGVRMeshTypeMonoscopic]; +} + +- (void)cleanUpTextures { + if (_texture) { + CFRelease(_texture); + _texture = NULL; + } + + if (_textureCache) { + CVOpenGLESTextureCacheFlush(_textureCache, 0); + } +} + +- (BOOL)hitTest:(GVRHeadPose *)headPose { + _hitTestPoint.x = nan(NULL); + _hitTestPoint.y = nan(NULL); + + GLKQuaternion headRotation = + GLKQuaternionMakeWithMatrix4(GLKMatrix4Transpose([headPose headTransform])); + + GLKVector3 cameraOrigin = GLKVector3Make(0.0f, 0.0f, 0.0f); + GLKVector3 cameraDirection = GLKQuaternionRotateVector3(headRotation, GLKVector3Make(0, 0, -1)); + cameraDirection = GLKVector3Normalize(cameraDirection); + + // Transform camera "ray" to our model space. + GLKMatrix4 modelSpace = GLKMatrix4Invert(self.position, nil); + GLKVector3 modelOrigin = GLKMatrix4MultiplyVector3WithTranslation(modelSpace, cameraOrigin); + GLKVector3 modelDirection = GLKMatrix4MultiplyVector3(modelSpace, cameraDirection); + + // If the ray is negative or only barely positive, then the ray is pointing away from the plane. + if (fabs(modelDirection.v[2]) < kDefaultEpsilon) { + return NO; + } + + // If bounding box is zero, the intersection point is 0,0. + GLKVector3 aabbDiff = GLKVector3Subtract(self.aabbMax, self.aabbMin); + if (GLKVector3Length(aabbDiff) < kDefaultEpsilon) { + return NO; + } + + const float lambda = -modelOrigin.v[2] / modelDirection.v[2]; + GLKVector3 delta = GLKVector3Add(modelOrigin, GLKVector3MultiplyScalar(modelDirection, lambda)); + GLKVector3 relativeDelta = + GLKVector3Divide(GLKVector3Subtract(delta, self.aabbMin), aabbDiff); + CGPoint intersection = CGPointMake(relativeDelta.v[0], relativeDelta.v[1]); + + intersection.y += -self.aabbMax.v[1] / aabbDiff.v[1]; + intersection.y *= -1; + + intersection.x *= _view.bounds.size.width; + intersection.y *= _view.bounds.size.height; + + if (intersection.x >= 0 && intersection.x < _view.bounds.size.width && + intersection.y >= 0 && intersection.y < _view.bounds.size.height) { + _hitTestPoint = intersection; + return YES; + } + + return NO; +} + + +@end diff --git a/Samples/GVRKit/GVRVideoRenderer.h b/Samples/GVRKit/GVRVideoRenderer.h new file mode 100644 index 0000000..8cd8357 --- /dev/null +++ b/Samples/GVRKit/GVRVideoRenderer.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRTextureRenderer.h" + +#import + +/** Defines a texture renderer for video frames. */ +@interface GVRVideoRenderer : GVRTextureRenderer + +/** The AVPlayer instance to grab video frames from. */ +@property(nonatomic) AVPlayer *player; + +@end diff --git a/Samples/GVRKit/GVRVideoRenderer.mm b/Samples/GVRKit/GVRVideoRenderer.mm new file mode 100644 index 0000000..bd02f25 --- /dev/null +++ b/Samples/GVRKit/GVRVideoRenderer.mm @@ -0,0 +1,215 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GVRVideoRenderer.h" + +#include +#include + +// Studio swing implies: +// Y values are in the range [16, 235]; +// Cb and Cr are [16, 240]. +// (see e.g. BT.601 Annex 1, Table 3, Row 8; BT.709 Section 4 ('Digital representation') Row 4.6) +// +// In OpenGL RGB land, all three channels are [0, 255]. +// +// The matrices below handle the necessary scale, origin and axis adjustments necessary to translate +// from the BT colour spaces to RGB. + +// BT.601 colorspace, studio swing. +static const GLKMatrix4 kColorConversionMatrix601 = { + 1.164, 1.164, 1.164, 0.0, 0.0, -0.392, 2.017, 0.0, // NOLINT + 1.596, -0.813, 0.0, 0.0, -0.874165, 0.531828, -1.08549, 1.0}; // NOLINT + +// BT.709 colorspace, studio swing. +static const GLKMatrix4 kColorConversionMatrix709 = { + 1.164, 1.164, 1.164, 0.0, 0.0, -0.213, 2.112, 0.0, // NOLINT + 1.793, -0.533, 0, 0.0, -0.973051, 0.301427, -1.13318, 1.0}; // NOLINT + +@interface GVRTextureRenderer (Subclassing) +- (void)setIsVideoTextureRenderer:(BOOL)isVideoTextureRenderer; +@end + +@implementation GVRVideoRenderer { + AVPlayer *_player; + AVPlayerItemVideoOutput *_videoOutput; + + CVOpenGLESTextureRef _lumaTexture; + CVOpenGLESTextureRef _chromaTexture; + CVOpenGLESTextureCacheRef _videoTextureCache; +} + +- (void)dealloc { + [_player.currentItem removeOutput:_videoOutput]; + [_player removeObserver:self forKeyPath:@"status"]; +} + +- (void)setPlayer:(AVPlayer *)player { + // Remove KVO from previous player. + [_player.currentItem removeOutput:_videoOutput]; + [_player removeObserver:self forKeyPath:@"status"]; + + _player = player; + + // Create a pixel buffer to hold AVPlayerItemVideoOutput. + NSDictionary *attributes = + @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)}; + _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes]; + + // Observe player's status property. + [_player addObserver:self forKeyPath:@"status" options:0 context:nil]; + if (_player.status == AVPlayerStatusReadyToPlay) { + [_player.currentItem addOutput:_videoOutput]; + } +} + +#pragma mark - Private + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if (object == _player && [keyPath isEqualToString:@"status"]) { + if (_player.status == AVPlayerStatusReadyToPlay) { + if (![_player.currentItem.outputs containsObject:_videoOutput]) { + [_player.currentItem addOutput:_videoOutput]; + } + } + } +} + +#pragma mark - GVRTextureRenderer + +- (void)initializeGl { + super.isVideoTextureRenderer = YES; + [super initializeGl]; + + // Create texture cache. + CVReturn err = CVOpenGLESTextureCacheCreate( + kCFAllocatorDefault, NULL, [EAGLContext currentContext], NULL, &_videoTextureCache); + NSAssert(err == noErr, @"Error at CVOpenGLESTextureCacheCreate %d", err); +} + +- (void)clearGl { + [super clearGl]; + + [self cleanUpTextures]; + if (_videoTextureCache) { + CFRelease(_videoTextureCache); + _videoTextureCache = NULL; + } +} + +- (void)update:(GVRHeadPose *)headPose { + CMTime itemTime = [_videoOutput itemTimeForHostTime:headPose.nextFrameTime]; + + if ([_videoOutput hasNewPixelBufferForItemTime:itemTime]) { + CVPixelBufferRef pixelBuffer = + [_videoOutput copyPixelBufferForItemTime:itemTime itemTimeForDisplay:NULL]; + if (pixelBuffer) { + [self cleanUpTextures]; + int videoWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int videoHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + BOOL requiresChannelSizes = EAGLContext.currentContext.API > kEAGLRenderingAPIOpenGLES2; + + // Create Y and UV textures from the pixel buffer. RGB is not supported. + _lumaTexture = [self createSourceTexture:pixelBuffer + index:0 + format:requiresChannelSizes ? GL_RED : GL_RED_EXT + internalFormat:requiresChannelSizes ? GL_R8 : GL_RED_EXT + width:videoWidth + height:videoHeight]; + // UV-plane. + _chromaTexture = [self createSourceTexture:pixelBuffer + index:1 + format:requiresChannelSizes ? GL_RG : GL_RG_EXT + internalFormat:requiresChannelSizes ? GL_RG8 : GL_RG_EXT + width:(videoWidth + 1) / 2 + height:(videoHeight + 1) / 2]; + + // Use the color attachment to determine the appropriate color conversion matrix. + CFTypeRef colorAttachments = + CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL); + GLKMatrix4 colorConversionMatrix = + CFEqual(colorAttachments, kCVImageBufferYCbCrMatrix_ITU_R_601_4) + ? kColorConversionMatrix601 + : kColorConversionMatrix709; + + GLuint lumaTextureId = CVOpenGLESTextureGetName(_lumaTexture); + GLuint chromaTextureId = CVOpenGLESTextureGetName(_chromaTexture); + [self setVideoYTextureId:lumaTextureId + uvTextureId:chromaTextureId + colorConversionMatrix:colorConversionMatrix]; + + CFRelease(pixelBuffer); + pixelBuffer = 0; + } + } + [super update:headPose]; +} + +- (CVOpenGLESTextureRef)createSourceTexture:(CVPixelBufferRef)pixelBuffer + index:(int)index + format:(GLint)format + internalFormat:(GLint)internalFormat + width:(int)width + height:(int)height { + CVOpenGLESTextureRef texture; + CVReturn err = CVOpenGLESTextureCacheCreateTextureFromImage( + kCFAllocatorDefault, // The CFAllocatorRef to use for allocating the texture object. + _videoTextureCache, // The texture cache object that will manage the texture. + pixelBuffer, // The CVImageBufferRef that you want to create a texture from. + NULL, // A CFDictionaryRef for creating the CVOpenGLESTextureRef objects. + GL_TEXTURE_2D, // The target texture. Can be GL_TEXTURE_2D or GL_RENDERBUFFER. + internalFormat, // The number of color components in the texture. + width, // The width of the texture image. + height, // The height of the texture image. + format, // The format of the pixel data. + GL_UNSIGNED_BYTE, // The data type of the pixel data. + index, // The plane of the CVImageBufferRef to map bind. + &texture); // Where the newly created texture object will be placed. + + if (err) { + NSLog(@"Could not create Texture, err = %d", err); + return NULL; + } + + glBindTexture(CVOpenGLESTextureGetTarget(texture), CVOpenGLESTextureGetName(texture)); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + return texture; +} + +- (void)cleanUpTextures { + if (_lumaTexture) { + CFRelease(_lumaTexture); + _lumaTexture = NULL; + } + + if (_chromaTexture) { + CFRelease(_chromaTexture); + _chromaTexture = NULL; + } + + if (_videoTextureCache) { + CVOpenGLESTextureCacheFlush(_videoTextureCache, 0); + } +} + +@end diff --git a/Samples/Panorama/Panorama-Info.plist b/Samples/Panorama/Panorama-Info.plist index 5fd3189..ab6d9ad 100644 --- a/Samples/Panorama/Panorama-Info.plist +++ b/Samples/Panorama/Panorama-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.google.${PRODUCT_NAME:rfc1034identifier} + ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Samples/Panorama/PanoramaViewController.m b/Samples/Panorama/PanoramaViewController.m index e8a6414..b311dee 100644 --- a/Samples/Panorama/PanoramaViewController.m +++ b/Samples/Panorama/PanoramaViewController.m @@ -1,16 +1,18 @@ #import "PanoramaViewController.h" -#import "GVRPanoramaView.h" +#import "GVROverlayView.h" +#import + +#import static const CGFloat kMargin = 16; static const CGFloat kPanoViewHeight = 250; -@interface PanoramaViewController () - +@interface PanoramaViewController () +@property(nonatomic) GVRRendererView *panoView; @end @implementation PanoramaViewController { - GVRPanoramaView *_panoView; UIScrollView *_scrollView; UILabel *_titleLabel; UILabel *_subtitleLabel; @@ -44,14 +46,11 @@ - (void)viewDidLoad { @"Mountains in Peru, above the Urubamba River valley."]; [_scrollView addSubview:_preambleLabel]; - _panoView = [[GVRPanoramaView alloc] init]; - _panoView.delegate = self; - _panoView.enableFullscreenButton = YES; - _panoView.enableCardboardButton = YES; - _panoView.enableTouchTracking = YES; - [_panoView loadImage:[UIImage imageNamed:@"andes.jpg"] - ofType:kGVRPanoramaImageTypeStereoOverUnder]; + GVRRendererViewController *viewController = [[GVRRendererViewController alloc] init]; + viewController.delegate = self; + _panoView = viewController.rendererView; [_scrollView addSubview:_panoView]; + [self addChildViewController:viewController]; _captionLabel = [self createLabelWithFontSize:14 text:@"A 360 panoramic view of Machu Picchu"]; _captionLabel.textColor = [UIColor darkGrayColor]; @@ -98,10 +97,6 @@ - (void)viewDidLoad { [_scrollView setAccessibilityIdentifier:@"sample_scroll_view"]; } -- (GVRPanoramaView *)getPanoramaView { - return _panoView; -} - - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; @@ -121,10 +116,27 @@ - (void)viewWillLayoutSubviews { CGRectGetMaxY(_attributionTextView.frame) + kMargin); } -#pragma mark - GVRWidgetViewDelegate +#pragma mark - GVRRendererViewControllerDelegate + +- (GVRRenderer *)rendererForDisplayMode:(GVRDisplayMode)displayMode { + UIImage *image = [UIImage imageNamed:@"andes.jpg"]; + GVRImageRenderer *imageRenderer = [[GVRImageRenderer alloc] initWithImage:image]; + [imageRenderer setSphericalMeshOfRadius:50 + latitudes:12 + longitudes:24 + verticalFov:180 + horizontalFov:360 + meshType:kGVRMeshTypeStereoTopBottom]; + + GVRSceneRenderer *sceneRenderer = [[GVRSceneRenderer alloc] init]; + [sceneRenderer.renderList addRenderObject:imageRenderer]; + + // Hide reticle in embedded display mode. + if (displayMode == kGVRDisplayModeEmbedded) { + sceneRenderer.hidesReticle = YES; + } -- (void)widgetView:(GVRWidgetView *)widgetView didLoadContent:(id)content { - NSLog(@"Loaded panorama image"); + return sceneRenderer; } #pragma mark - Implementation diff --git a/Samples/Panorama/Podfile b/Samples/Panorama/Podfile index b8f890a..f6a24dd 100644 --- a/Samples/Panorama/Podfile +++ b/Samples/Panorama/Podfile @@ -1,3 +1,3 @@ target 'Panorama' do - pod 'GVRSDK' + pod 'GVRKit' end diff --git a/Samples/Stars/Podfile b/Samples/Stars/Podfile index baa9856..e950463 100644 --- a/Samples/Stars/Podfile +++ b/Samples/Stars/Podfile @@ -1,3 +1,3 @@ target 'Stars' do - pod 'GVRSDK' + pod 'GVRKit' end diff --git a/Samples/Stars/Stars-Info.plist b/Samples/Stars/Stars-Info.plist index 8de64fa..4f18a4d 100644 --- a/Samples/Stars/Stars-Info.plist +++ b/Samples/Stars/Stars-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.google.${PRODUCT_NAME:rfc1034identifier} + ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Samples/Stars/Stars.xcodeproj/project.pbxproj b/Samples/Stars/Stars.xcodeproj/project.pbxproj index a3a4b03..9e9a1dc 100644 --- a/Samples/Stars/Stars.xcodeproj/project.pbxproj +++ b/Samples/Stars/Stars.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 1237AAB11C86750900AD3514 /* Stars-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1237AAB01C86750900AD3514 /* Stars-Info.plist */; }; 1237AABA1C86751900AD3514 /* StarsAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1237AAB31C86751900AD3514 /* StarsAppDelegate.m */; }; 1237AABB1C86751900AD3514 /* StarsRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1237AAB51C86751900AD3514 /* StarsRenderer.m */; }; - 1237AABC1C86751900AD3514 /* StarsRenderLoop.m in Sources */ = {isa = PBXBuildFile; fileRef = 1237AAB71C86751900AD3514 /* StarsRenderLoop.m */; }; 1237AABD1C86751900AD3514 /* StarsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1237AAB91C86751900AD3514 /* StarsViewController.m */; }; 1237AABF1C86753000AD3514 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1237AABE1C86753000AD3514 /* main.m */; }; 1237AAC11C86755100AD3514 /* launch.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1237AAC01C86755100AD3514 /* launch.xib */; }; @@ -25,8 +24,6 @@ 1237AAB31C86751900AD3514 /* StarsAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StarsAppDelegate.m; sourceTree = SOURCE_ROOT; }; 1237AAB41C86751900AD3514 /* StarsRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StarsRenderer.h; sourceTree = SOURCE_ROOT; }; 1237AAB51C86751900AD3514 /* StarsRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StarsRenderer.m; sourceTree = SOURCE_ROOT; }; - 1237AAB61C86751900AD3514 /* StarsRenderLoop.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StarsRenderLoop.h; sourceTree = SOURCE_ROOT; }; - 1237AAB71C86751900AD3514 /* StarsRenderLoop.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StarsRenderLoop.m; sourceTree = SOURCE_ROOT; }; 1237AAB81C86751900AD3514 /* StarsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StarsViewController.h; sourceTree = SOURCE_ROOT; }; 1237AAB91C86751900AD3514 /* StarsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StarsViewController.m; sourceTree = SOURCE_ROOT; }; 1237AABE1C86753000AD3514 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; }; @@ -72,8 +69,6 @@ 1237AAB31C86751900AD3514 /* StarsAppDelegate.m */, 1237AAB41C86751900AD3514 /* StarsRenderer.h */, 1237AAB51C86751900AD3514 /* StarsRenderer.m */, - 1237AAB61C86751900AD3514 /* StarsRenderLoop.h */, - 1237AAB71C86751900AD3514 /* StarsRenderLoop.m */, 1237AAB81C86751900AD3514 /* StarsViewController.h */, 1237AAB91C86751900AD3514 /* StarsViewController.m */, 1237AAB01C86750900AD3514 /* Stars-Info.plist */, @@ -171,7 +166,6 @@ 1237AABF1C86753000AD3514 /* main.m in Sources */, 1237AABB1C86751900AD3514 /* StarsRenderer.m in Sources */, 1237AABA1C86751900AD3514 /* StarsAppDelegate.m in Sources */, - 1237AABC1C86751900AD3514 /* StarsRenderLoop.m in Sources */, 1237AABD1C86751900AD3514 /* StarsViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Samples/Stars/StarsAppDelegate.m b/Samples/Stars/StarsAppDelegate.m index 1f1032b..395c187 100644 --- a/Samples/Stars/StarsAppDelegate.m +++ b/Samples/Stars/StarsAppDelegate.m @@ -6,34 +6,16 @@ #import "StarsViewController.h" -@interface StarsAppDelegate () - -@end - @implementation StarsAppDelegate #pragma mark - UIApplicationDelegate overrides - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - UINavigationController *navigationController = [[UINavigationController alloc] - initWithRootViewController:[[StarsViewController alloc] init]]; - navigationController.delegate = self; - navigationController.navigationBarHidden = YES; - self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - self.window.rootViewController = navigationController; + self.window.rootViewController = [[StarsViewController alloc] init]; [self.window makeKeyAndVisible]; return YES; } -#pragma mark - UINavigationControllerDelegate - -// Make the navigation controller defer the check of supported orientation to its topmost view -// controller. This allows |GVRCardboardViewController| to lock the orientation in VR mode. -- (UIInterfaceOrientationMask)navigationControllerSupportedInterfaceOrientations: - (UINavigationController *)navigationController { - return [navigationController.topViewController supportedInterfaceOrientations]; -} - @end diff --git a/Samples/Stars/StarsRenderLoop.h b/Samples/Stars/StarsRenderLoop.h deleted file mode 100644 index 4c70f4b..0000000 --- a/Samples/Stars/StarsRenderLoop.h +++ /dev/null @@ -1,23 +0,0 @@ -#import - -@interface StarsRenderLoop : NSObject - -/** - * Initializes the render loop with target and selector. The underlying |CADisplayLink| instance - * holds a strong reference to the target until the |invalidate| method is called. - */ -- (instancetype)initWithRenderTarget:(id)target selector:(SEL)selector; - -/** - * Invalidates this instance and the underlying |CADisplayLink| instance releases its strong - * reference to the render target. - */ -- (void)invalidate; - -/** Sets or returns the paused state of the underlying |CADisplayLink| reference. */ -@property(nonatomic) BOOL paused; - -/** Returns the timestamp of when the next frame will be drawn. */ -@property(nonatomic, readonly) NSTimeInterval nextFrameTime; - -@end diff --git a/Samples/Stars/StarsRenderLoop.m b/Samples/Stars/StarsRenderLoop.m deleted file mode 100644 index e5591a0..0000000 --- a/Samples/Stars/StarsRenderLoop.m +++ /dev/null @@ -1,113 +0,0 @@ -#import "StarsRenderLoop.h" - -// Flag to control if rendering is performed on a background thread or on the main thread. -static const BOOL kRenderInBackgroundThread = YES; - -@implementation StarsRenderLoop { - NSThread *_renderThread; - CADisplayLink *_displayLink; - BOOL _paused; -} - -- (instancetype)initWithRenderTarget:(id)target selector:(SEL)selector { - if (self = [super init]) { - _displayLink = [CADisplayLink displayLinkWithTarget:target selector:selector]; - - if (kRenderInBackgroundThread) { - _renderThread = [[NSThread alloc] initWithTarget:self - selector:@selector(threadMain) - object:nil]; - [_renderThread start]; - } else { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidBecomeActive:) - name:UIApplicationDidBecomeActiveNotification - object:nil]; - } - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Public - -- (void)invalidate { - if (kRenderInBackgroundThread) { - [self performSelector:@selector(renderThreadStop) - onThread:_renderThread - withObject:nil - waitUntilDone:NO]; - } else { - [_displayLink invalidate]; - _displayLink = nil; - } -} - -- (BOOL)paused { - return _paused; -} - -- (void)setPaused:(BOOL)paused { - _paused = paused; - _displayLink.paused = paused; -} - -- (NSTimeInterval)nextFrameTime { - return _displayLink.timestamp + (_displayLink.duration * _displayLink.frameInterval); -} - -#pragma mark - NSNotificationCenter - -- (void)applicationWillResignActive:(NSNotification *)notification { - if (kRenderInBackgroundThread) { - [self performSelector:@selector(threadPause) - onThread:_renderThread - withObject:nil - waitUntilDone:YES]; - } else { - [self threadPause]; - } -} - -- (void)applicationDidBecomeActive:(NSNotification *)notification { - _displayLink.paused = _paused; -} - -#pragma mark - Background thread rendering. - -- (void)threadMain { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - - CFRunLoopRun(); -} - -- (void)threadPause { - _displayLink.paused = YES; -} - -- (void)renderThreadStop { - [_displayLink invalidate]; - _displayLink = nil; - - CFRunLoopStop(CFRunLoopGetCurrent()); - - // Ensure we release the last reference to self on the main thread. The ivar: _renderThread in the - // dispatch block implicitly retains a strong reference to self. - // See 'The deallocation problem' in: - // https://developer.apple.com/library/ios/technotes/tn2109/_index.html - dispatch_async(dispatch_get_main_queue(), ^{ - [_renderThread cancel]; - _renderThread = nil; - }); -} - -@end diff --git a/Samples/Stars/StarsRenderer.h b/Samples/Stars/StarsRenderer.h index 2e65b3c..adddf9e 100644 --- a/Samples/Stars/StarsRenderer.h +++ b/Samples/Stars/StarsRenderer.h @@ -1,10 +1,6 @@ -#import "GVRCardboardView.h" +#import -@class StarsRenderLoop; - -/** Cardboard Stars renderer. */ -@interface StarsRenderer : NSObject - -@property(nonatomic, weak) StarsRenderLoop *renderLoop; +/** Stars renderer. */ +@interface StarsRenderer : GVRRenderer @end diff --git a/Samples/Stars/StarsRenderer.m b/Samples/Stars/StarsRenderer.m index 8bb0a78..324af58 100644 --- a/Samples/Stars/StarsRenderer.m +++ b/Samples/Stars/StarsRenderer.m @@ -4,16 +4,6 @@ #import "StarsRenderer.h" -#import "StarsRenderLoop.h" - -#import -#import -#import -#import -#import - -#import "GVRHeadTransform.h" - #define VERTEX_COUNT 200000 typedef NS_ENUM(NSUInteger, EngineMode) { @@ -149,80 +139,10 @@ @implementation StarsRenderer { GLuint _vertex_buffer; } -#pragma mark - GVRCardboardViewDelegate overrides - -- (void)cardboardView:(GVRCardboardView *)cardboardView - prepareDrawFrame:(GVRHeadTransform *)headTransform { - NSTimeInterval timestep = _renderLoop.nextFrameTime - _last_timestamp; - if (timestep > 1.0) { - timestep = 1.0; - } - _last_timestamp = _renderLoop.nextFrameTime; - - // Accelerate when our engines are on and we're not in warp mode. - if (_engine_mode == EngineModeImpulse || _engine_mode == EngineModeToWarp) { - float thrust_vector[3]; - thrust_vector[0] = 0.0f; - thrust_vector[1] = 0.0f; - thrust_vector[2] = 0.02 * (1.0 + 1000.f * _warp_factor) * timestep; - GLKMatrix4 headPoseInStartSpace = GLKMatrix4Transpose([headTransform headPoseInStartSpace]); - float *head_matrix = headPoseInStartSpace.m; - _offset_velocity[0] += thrust_vector[0] * head_matrix[0] + - thrust_vector[1] * head_matrix[4] + - thrust_vector[2] * head_matrix[8]; - _offset_velocity[1] += thrust_vector[0] * head_matrix[1] + - thrust_vector[1] * head_matrix[5] + - thrust_vector[2] * head_matrix[9]; - _offset_velocity[2] += thrust_vector[0] * head_matrix[2] + - thrust_vector[1] * head_matrix[6] + - thrust_vector[2] * head_matrix[10]; - } - - // Slow down if we're not in warp. - if (_engine_mode != EngineModeWarp) { - float speed = sqrt( - _offset_velocity[0] * _offset_velocity[0] + - _offset_velocity[1] * _offset_velocity[1] + - _offset_velocity[2] * _offset_velocity[2]); - float max_speed = .07f + 5.0f * _warp_factor; - const float drag = 0.995f * (max_speed / fmax(speed, max_speed)); - _offset_velocity[0] *= drag; - _offset_velocity[1] *= drag; - _offset_velocity[2] *= drag; - } - _offset_position[0] += _offset_velocity[0]; - _offset_position[1] += _offset_velocity[1]; - _offset_position[2] += _offset_velocity[2]; - _offset_position[0] = fmod(_offset_position[0], 200.0f); - _offset_position[1] = fmod(_offset_position[1], 200.0f); - _offset_position[2] = fmod(_offset_position[2], 200.0f); - - // Adjust our warp factor if needed. - if (_engine_mode == EngineModeToWarp) { - _warp_factor += 0.01f; - if (_warp_factor >= 1.0) { - _warp_factor = 1.0f; - _engine_mode = EngineModeWarp; - } - } else if (_engine_mode == EngineModeToImpulse) { - _warp_factor -= 0.01f; - if (_warp_factor <= 0.0) { - _warp_factor = 0.0f; - _engine_mode = EngineModeImpulse; - } - } - - glDisable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE); - glDisable(GL_SCISSOR_TEST); - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - glEnable(GL_SCISSOR_TEST); -} +#pragma mark - GVRRenderer overrides -- (void)cardboardView:(GVRCardboardView *)cardboardView - willStartDrawing:(GVRHeadTransform *)headTransform { +- (void)initializeGl { + [super initializeGl]; // Renderer must be created on GL thread before any call to drawFrame. // Load the vertex/fragment shaders. const GLuint vertex_shader = LoadShader(GL_VERTEX_SHADER, kVertexShaderString); @@ -306,19 +226,86 @@ - (void)cardboardView:(GVRCardboardView *)cardboardView glBufferData(GL_ARRAY_BUFFER, sizeof(_vertices), _vertices, GL_STATIC_DRAW); } -- (void)cardboardView:(GVRCardboardView *)cardboardView - drawEye:(GVREye)eye - withHeadTransform:(GVRHeadTransform *)headTransform { - CGRect viewport = [headTransform viewportForEye:eye]; +- (void)update:(GVRHeadPose *)headPose { + NSTimeInterval timestep = headPose.nextFrameTime - _last_timestamp; + if (timestep > 1.0) { + timestep = 1.0; + } + _last_timestamp = headPose.nextFrameTime; + + // Accelerate when our engines are on and we're not in warp mode. + if (_engine_mode == EngineModeImpulse || _engine_mode == EngineModeToWarp) { + float thrust_vector[3]; + thrust_vector[0] = 0.0f; + thrust_vector[1] = 0.0f; + thrust_vector[2] = 0.02 * (1.0 + 1000.f * _warp_factor) * timestep; + GLKMatrix4 headPoseInStartSpace = GLKMatrix4Transpose([headPose headTransform]); + float *head_matrix = headPoseInStartSpace.m; + _offset_velocity[0] += thrust_vector[0] * head_matrix[0] + + thrust_vector[1] * head_matrix[4] + + thrust_vector[2] * head_matrix[8]; + _offset_velocity[1] += thrust_vector[0] * head_matrix[1] + + thrust_vector[1] * head_matrix[5] + + thrust_vector[2] * head_matrix[9]; + _offset_velocity[2] += thrust_vector[0] * head_matrix[2] + + thrust_vector[1] * head_matrix[6] + + thrust_vector[2] * head_matrix[10]; + } + + // Slow down if we're not in warp. + if (_engine_mode != EngineModeWarp) { + float speed = sqrt( + _offset_velocity[0] * _offset_velocity[0] + + _offset_velocity[1] * _offset_velocity[1] + + _offset_velocity[2] * _offset_velocity[2]); + float max_speed = .07f + 5.0f * _warp_factor; + const float drag = 0.995f * (max_speed / fmax(speed, max_speed)); + _offset_velocity[0] *= drag; + _offset_velocity[1] *= drag; + _offset_velocity[2] *= drag; + } + _offset_position[0] += _offset_velocity[0]; + _offset_position[1] += _offset_velocity[1]; + _offset_position[2] += _offset_velocity[2]; + _offset_position[0] = fmod(_offset_position[0], 200.0f); + _offset_position[1] = fmod(_offset_position[1], 200.0f); + _offset_position[2] = fmod(_offset_position[2], 200.0f); + + // Adjust our warp factor if needed. + if (_engine_mode == EngineModeToWarp) { + _warp_factor += 0.01f; + if (_warp_factor >= 1.0) { + _warp_factor = 1.0f; + _engine_mode = EngineModeWarp; + } + } else if (_engine_mode == EngineModeToImpulse) { + _warp_factor -= 0.01f; + if (_warp_factor <= 0.0) { + _warp_factor = 0.0f; + _engine_mode = EngineModeImpulse; + } + } + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glDisable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE); + glEnable(GL_SCISSOR_TEST); +} + +- (void)draw:(GVRHeadPose *)headPose { + CGRect viewport = [headPose viewport]; glViewport(viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height); glScissor(viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height); + glClear(GL_COLOR_BUFFER_BIT); + // Get the head matrix. - const GLKMatrix4 head_from_start_matrix = [headTransform headPoseInStartSpace]; + const GLKMatrix4 head_from_start_matrix = [headPose headTransform]; // Get this eye's matrices. - GLKMatrix4 projection_matrix = [headTransform projectionMatrixForEye:eye near:0.1f far:100.0f]; - GLKMatrix4 eye_from_head_matrix = [headTransform eyeFromHeadMatrix:eye]; + GLKMatrix4 projection_matrix = [headPose projectionMatrixWithNear:0.1f far:100.0f]; + GLKMatrix4 eye_from_head_matrix = [headPose eyeTransform]; // Render from this eye. [self renderWithProjectionMatrix:projection_matrix.m @@ -360,32 +347,18 @@ - (void)renderWithProjectionMatrix:(const float *)projection_matrix glDisableVertexAttribArray(_attrib_color); } -- (void)cardboardView:(GVRCardboardView *)cardboardView - didFireEvent:(GVRUserEvent)event { - switch(event) { - case kGVRUserEventBackButton: - NSLog(@"User pressed back button"); +- (BOOL)handleTrigger { + switch (_engine_mode) { + case EngineModeImpulse: + case EngineModeToImpulse: + _engine_mode = EngineModeToWarp; break; - case kGVRUserEventTilt: - NSLog(@"User performed tilt action"); - break; - case kGVRUserEventTrigger: - switch (_engine_mode) { - case EngineModeImpulse: - case EngineModeToImpulse: - _engine_mode = EngineModeToWarp; - break; - - default: - _engine_mode = EngineModeToImpulse; - break; - } + + default: + _engine_mode = EngineModeToImpulse; break; } -} - -- (void)cardboardView:(GVRCardboardView *)cardboardView shouldPauseDrawing:(BOOL)pause { - _renderLoop.paused = pause; + return YES; } @end diff --git a/Samples/Stars/StarsViewController.h b/Samples/Stars/StarsViewController.h index 9fc581c..1ec95dc 100644 --- a/Samples/Stars/StarsViewController.h +++ b/Samples/Stars/StarsViewController.h @@ -1,5 +1,6 @@ +#import #import -@interface StarsViewController : UIViewController +@interface StarsViewController : GVRRendererViewController @end diff --git a/Samples/Stars/StarsViewController.m b/Samples/Stars/StarsViewController.m index df7fc79..7b1da63 100644 --- a/Samples/Stars/StarsViewController.m +++ b/Samples/Stars/StarsViewController.m @@ -1,55 +1,21 @@ #import "StarsViewController.h" -#import "StarsRenderLoop.h" #import "StarsRenderer.h" -@interface StarsViewController () { - GVRCardboardView *_cardboardView; - StarsRenderer *_starsRenderer; - StarsRenderLoop *_renderLoop; -} -@end - @implementation StarsViewController -- (void)loadView { - _starsRenderer = [[StarsRenderer alloc] init]; - - _cardboardView = [[GVRCardboardView alloc] initWithFrame:CGRectZero]; - _cardboardView.delegate = _starsRenderer; - _cardboardView.autoresizingMask = - UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - - _cardboardView.vrModeEnabled = YES; - - // Use double-tap gesture to toggle between VR and magic window mode. - UITapGestureRecognizer *doubleTapGesture = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapView:)]; - doubleTapGesture.numberOfTapsRequired = 2; - [_cardboardView addGestureRecognizer:doubleTapGesture]; - - self.view = _cardboardView; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - - _renderLoop = [[StarsRenderLoop alloc] initWithRenderTarget:_cardboardView - selector:@selector(render)]; - _starsRenderer.renderLoop = _renderLoop; +- (instancetype)init { + return [super initWithRenderer:[[StarsRenderer alloc] init]]; } -- (void)viewDidDisappear:(BOOL)animated { - [super viewDidDisappear:animated]; - - [_renderLoop invalidate]; - _renderLoop = nil; +- (BOOL)isModal { + // Return YES since we are the topmost fullscreen view controller. + return YES; } -#pragma mark - Implementation - -- (void)didDoubleTapView:(id)sender { - _cardboardView.vrModeEnabled = !_cardboardView.vrModeEnabled; +- (void)didTapBackButton { + // User pressed the back button. Pop this view controller. + NSLog(@"User pressed back button"); } @end diff --git a/Samples/TreasureHunt/Podfile b/Samples/TreasureHunt/Podfile index 4cf29c9..ee2deed 100644 --- a/Samples/TreasureHunt/Podfile +++ b/Samples/TreasureHunt/Podfile @@ -1,3 +1,3 @@ target 'TreasureHunt' do - pod 'GVRSDK' + pod 'GVRKit' end diff --git a/Samples/TreasureHunt/TreasureHunt-Info.plist b/Samples/TreasureHunt/TreasureHunt-Info.plist index 8e3f4cd..7a7a68b 100644 --- a/Samples/TreasureHunt/TreasureHunt-Info.plist +++ b/Samples/TreasureHunt/TreasureHunt-Info.plist @@ -11,7 +11,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.google.${PRODUCT_NAME:rfc1034identifier} + ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Samples/TreasureHunt/TreasureHunt.xcodeproj/project.pbxproj b/Samples/TreasureHunt/TreasureHunt.xcodeproj/project.pbxproj index 3465c4d..5a176e2 100644 --- a/Samples/TreasureHunt/TreasureHunt.xcodeproj/project.pbxproj +++ b/Samples/TreasureHunt/TreasureHunt.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 125D19BC1D875307003B793D /* cube_sound.wav in Resources */ = {isa = PBXBuildFile; fileRef = 125D19BA1D875307003B793D /* cube_sound.wav */; }; 125D19BD1D875307003B793D /* success.wav in Resources */ = {isa = PBXBuildFile; fileRef = 125D19BB1D875307003B793D /* success.wav */; }; 12BEE9C01C8F5426002F8361 /* TreasureHuntAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 12BEE9B91C8F5426002F8361 /* TreasureHuntAppDelegate.m */; }; - 12BEE9C21C8F5426002F8361 /* TreasureHuntRenderLoop.m in Sources */ = {isa = PBXBuildFile; fileRef = 12BEE9BD1C8F5426002F8361 /* TreasureHuntRenderLoop.m */; }; 12BEE9C31C8F5426002F8361 /* TreasureHuntViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 12BEE9BF1C8F5426002F8361 /* TreasureHuntViewController.m */; }; 12BEE9C81C8F546B002F8361 /* launch.xib in Resources */ = {isa = PBXBuildFile; fileRef = 12BEE9C61C8F546B002F8361 /* launch.xib */; }; 12BEE9CB1C8F54B8002F8361 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 12BEE9CA1C8F54B8002F8361 /* main.m */; }; @@ -25,8 +24,6 @@ 12BEE99E1C8F53AB002F8361 /* TreasureHunt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TreasureHunt.app; sourceTree = BUILT_PRODUCTS_DIR; }; 12BEE9B81C8F5426002F8361 /* TreasureHuntAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TreasureHuntAppDelegate.h; sourceTree = SOURCE_ROOT; }; 12BEE9B91C8F5426002F8361 /* TreasureHuntAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TreasureHuntAppDelegate.m; sourceTree = SOURCE_ROOT; }; - 12BEE9BC1C8F5426002F8361 /* TreasureHuntRenderLoop.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TreasureHuntRenderLoop.h; sourceTree = SOURCE_ROOT; }; - 12BEE9BD1C8F5426002F8361 /* TreasureHuntRenderLoop.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TreasureHuntRenderLoop.m; sourceTree = SOURCE_ROOT; }; 12BEE9BE1C8F5426002F8361 /* TreasureHuntViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TreasureHuntViewController.h; sourceTree = SOURCE_ROOT; }; 12BEE9BF1C8F5426002F8361 /* TreasureHuntViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TreasureHuntViewController.m; sourceTree = SOURCE_ROOT; }; 12BEE9C41C8F5432002F8361 /* TreasureHunt-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "TreasureHunt-Info.plist"; sourceTree = SOURCE_ROOT; }; @@ -69,8 +66,6 @@ 12BEE9C61C8F546B002F8361 /* launch.xib */, 12BEE9B81C8F5426002F8361 /* TreasureHuntAppDelegate.h */, 12BEE9B91C8F5426002F8361 /* TreasureHuntAppDelegate.m */, - 12BEE9BC1C8F5426002F8361 /* TreasureHuntRenderLoop.h */, - 12BEE9BD1C8F5426002F8361 /* TreasureHuntRenderLoop.m */, 122508B21C99FFC50092EC1B /* TreasureHuntRenderer.h */, 122508B31C99FFC50092EC1B /* TreasureHuntRenderer.m */, 12BEE9BE1C8F5426002F8361 /* TreasureHuntViewController.h */, @@ -161,7 +156,6 @@ 12BEE9CB1C8F54B8002F8361 /* main.m in Sources */, 12BEE9C31C8F5426002F8361 /* TreasureHuntViewController.m in Sources */, 12BEE9C01C8F5426002F8361 /* TreasureHuntAppDelegate.m in Sources */, - 12BEE9C21C8F5426002F8361 /* TreasureHuntRenderLoop.m in Sources */, 122508B41C99FFC50092EC1B /* TreasureHuntRenderer.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Samples/TreasureHunt/TreasureHuntAppDelegate.m b/Samples/TreasureHunt/TreasureHuntAppDelegate.m index 099213b..272b610 100644 --- a/Samples/TreasureHunt/TreasureHuntAppDelegate.m +++ b/Samples/TreasureHunt/TreasureHuntAppDelegate.m @@ -6,10 +6,6 @@ #import "TreasureHuntViewController.h" -@interface TreasureHuntAppDelegate () - -@end - @implementation TreasureHuntAppDelegate #pragma mark - UIApplicationDelegate overrides @@ -18,7 +14,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:[[TreasureHuntViewController alloc] init]]; - navigationController.delegate = self; navigationController.navigationBarHidden = YES; self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; @@ -27,13 +22,4 @@ - (BOOL)application:(UIApplication *)application return YES; } -#pragma mark - UINavigationControllerDelegate - -// Make the navigation controller defer the check of supported orientation to its topmost view -// controller. This allows |GVRCardboardViewController| to lock the orientation in VR mode. -- (UIInterfaceOrientationMask)navigationControllerSupportedInterfaceOrientations: - (UINavigationController *)navigationController { - return [navigationController.topViewController supportedInterfaceOrientations]; -} - @end diff --git a/Samples/TreasureHunt/TreasureHuntRenderLoop.h b/Samples/TreasureHunt/TreasureHuntRenderLoop.h deleted file mode 100644 index 2f93d04..0000000 --- a/Samples/TreasureHunt/TreasureHuntRenderLoop.h +++ /dev/null @@ -1,21 +0,0 @@ -#import - -@interface TreasureHuntRenderLoop : NSObject - -/** - * Initializes the render loop with target and selector. The underlying |CADisplayLink| instance - * holds a strong reference to the target until the |invalidate| method is called. - */ -- (instancetype)initWithRenderTarget:(id)target selector:(SEL)selector; - -/** - * Invalidates this instance and the underlying |CADisplayLink| instance releases its strong - * reference to the render target. - */ -- (void)invalidate; - -/** Sets or returns the paused state of the underlying |CADisplayLink| reference. */ -@property(nonatomic) BOOL paused; - -@end - diff --git a/Samples/TreasureHunt/TreasureHuntRenderLoop.m b/Samples/TreasureHunt/TreasureHuntRenderLoop.m deleted file mode 100644 index 311eec1..0000000 --- a/Samples/TreasureHunt/TreasureHuntRenderLoop.m +++ /dev/null @@ -1,109 +0,0 @@ -#import "TreasureHuntRenderLoop.h" - -// Flag to control if rendering is performed on a background thread or on the main thread. -static const BOOL kRenderInBackgroundThread = YES; - -@implementation TreasureHuntRenderLoop { - NSThread *_renderThread; - CADisplayLink *_displayLink; - BOOL _paused; -} - -- (instancetype)initWithRenderTarget:(id)target selector:(SEL)selector { - if (self = [super init]) { - _displayLink = [CADisplayLink displayLinkWithTarget:target selector:selector]; - - if (kRenderInBackgroundThread) { - _renderThread = [[NSThread alloc] initWithTarget:self - selector:@selector(threadMain) - object:nil]; - [_renderThread start]; - } else { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - } - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationDidBecomeActive:) - name:UIApplicationDidBecomeActiveNotification - object:nil]; - } - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Public - -- (void)invalidate { - if (kRenderInBackgroundThread) { - [self performSelector:@selector(renderThreadStop) - onThread:_renderThread - withObject:nil - waitUntilDone:NO]; - } else { - [_displayLink invalidate]; - _displayLink = nil; - } -} - -- (BOOL)paused { - return _paused; -} - -- (void)setPaused:(BOOL)paused { - _paused = paused; - _displayLink.paused = paused; -} - -#pragma mark - NSNotificationCenter - -- (void)applicationWillResignActive:(NSNotification *)notification { - if (kRenderInBackgroundThread) { - [self performSelector:@selector(threadPause) - onThread:_renderThread - withObject:nil - waitUntilDone:YES]; - } else { - [self threadPause]; - } -} - -- (void)applicationDidBecomeActive:(NSNotification *)notification { - _displayLink.paused = _paused; -} - -#pragma mark - Background thread rendering. - -- (void)threadMain { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - - CFRunLoopRun(); -} - -- (void)threadPause { - _displayLink.paused = YES; -} - -- (void)renderThreadStop { - [_displayLink invalidate]; - _displayLink = nil; - - CFRunLoopStop(CFRunLoopGetCurrent()); - - // Ensure we release the last reference to self on the main thread. The ivar: _renderThread in the - // dispatch block implicitly retains a strong reference to self. - // See 'The deallocation problem' in: - // https://developer.apple.com/library/ios/technotes/tn2109/_index.html - dispatch_async(dispatch_get_main_queue(), ^{ - [_renderThread cancel]; - _renderThread = nil; - }); -} - -@end diff --git a/Samples/TreasureHunt/TreasureHuntRenderer.h b/Samples/TreasureHunt/TreasureHuntRenderer.h index 94ffdea..2bb96e1 100644 --- a/Samples/TreasureHunt/TreasureHuntRenderer.h +++ b/Samples/TreasureHunt/TreasureHuntRenderer.h @@ -1,18 +1,9 @@ -#import "GVRCardboardView.h" - -/** TreasureHunt renderer delegate. */ -@protocol TreasureHuntRendererDelegate -@optional - -/** Called to pause the render loop because a 2D UI is overlaid on top of the renderer. */ -- (void)shouldPauseRenderLoop:(BOOL)pause; - -@end +#import /** TreasureHunt renderer. */ -@interface TreasureHuntRenderer : NSObject +@interface TreasureHuntRenderer : GVRRenderer -@property(nonatomic, weak) id delegate; +- (void)handleTrigger; @end diff --git a/Samples/TreasureHunt/TreasureHuntRenderer.m b/Samples/TreasureHunt/TreasureHuntRenderer.m index 62b0ef1..a141497 100644 --- a/Samples/TreasureHunt/TreasureHuntRenderer.m +++ b/Samples/TreasureHunt/TreasureHuntRenderer.m @@ -17,7 +17,6 @@ #import #import "GVRAudioEngine.h" -#import "GVRHeadTransform.h" // Vertex shader implementation. static const char *kVertexShaderString = @@ -362,6 +361,14 @@ static bool checkProgramLinkStatus(GLuint shader_program) { return true; } +static void CheckGLError(const char *label) { + int gl_error = glGetError(); + if (gl_error != GL_NO_ERROR) { + NSLog(@"GL error %s: %d", label, gl_error); + } + assert(glGetError() == GL_NO_ERROR); +} + @implementation TreasureHuntRenderer { // GL variables for the cube. GLfloat _cube_vertices[NUM_CUBE_VERTICES]; @@ -397,10 +404,14 @@ @implementation TreasureHuntRenderer { bool _is_cube_focused; } -#pragma mark - GVRCardboardViewDelegate overrides +- (void)dealloc { + [_gvr_audio_engine stopSound:_sound_object_id]; + [_gvr_audio_engine stop]; +} + +- (void)initializeGl { + [super initializeGl]; -- (void)cardboardView:(GVRCardboardView *)cardboardView - willStartDrawing:(GVRHeadTransform *)headTransform { // Renderer must be created on GL thread before any call to drawFrame. // Load the vertex/fragment shaders. const GLuint vertex_shader = LoadShader(GL_VERTEX_SHADER, kVertexShaderString); @@ -516,13 +527,20 @@ - (void)cardboardView:(GVRCardboardView *)cardboardView _sound_object_id = [_gvr_audio_engine createSoundObject:kObjectSoundFile]; [self spawnCube]; + CheckGLError("init"); +} + +- (void)clearGl { + [_gvr_audio_engine stopSound:_sound_object_id]; + [_gvr_audio_engine stop]; + + [super clearGl]; } -- (void)cardboardView:(GVRCardboardView *)cardboardView - prepareDrawFrame:(GVRHeadTransform *)headTransform { +- (void)update:(GVRHeadPose *)headPose { // Update audio listener's head rotation. const GLKQuaternion head_rotation = - GLKQuaternionMakeWithMatrix4(GLKMatrix4Transpose([headTransform headPoseInStartSpace])); + GLKQuaternionMakeWithMatrix4(GLKMatrix4Transpose([headPose headTransform])); [_gvr_audio_engine setHeadRotation:head_rotation.q[0] y:head_rotation.q[1] z:head_rotation.q[2] @@ -538,23 +556,24 @@ - (void)cardboardView:(GVRCardboardView *)cardboardView // Clear GL viewport. glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glEnable(GL_DEPTH_TEST); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_SCISSOR_TEST); + CheckGLError("update"); } -- (void)cardboardView:(GVRCardboardView *)cardboardView - drawEye:(GVREye)eye - withHeadTransform:(GVRHeadTransform *)headTransform { - CGRect viewport = [headTransform viewportForEye:eye]; +- (void)draw:(GVRHeadPose *)headPose { + CGRect viewport = [headPose viewport]; glViewport(viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height); glScissor(viewport.origin.x, viewport.origin.y, viewport.size.width, viewport.size.height); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + CheckGLError("glClear"); + // Get the head matrix. - const GLKMatrix4 head_from_start_matrix = [headTransform headPoseInStartSpace]; + const GLKMatrix4 head_from_start_matrix = [headPose headTransform]; // Get this eye's matrices. - GLKMatrix4 projection_matrix = [headTransform projectionMatrixForEye:eye near:0.1f far:100.0f]; - GLKMatrix4 eye_from_head_matrix = [headTransform eyeFromHeadMatrix:eye]; + GLKMatrix4 projection_matrix = [headPose projectionMatrixWithNear:0.1f far:100.0f]; + GLKMatrix4 eye_from_head_matrix = [headPose eyeTransform]; // Compute the model view projection matrix. GLKMatrix4 model_view_projection_matrix = GLKMatrix4Multiply( @@ -562,11 +581,13 @@ - (void)cardboardView:(GVRCardboardView *)cardboardView // Render from this eye. [self renderWithModelViewProjectionMatrix:model_view_projection_matrix.m]; + CheckGLError("render"); } - (void)renderWithModelViewProjectionMatrix:(const float *)model_view_matrix { // Select our shader. glUseProgram(_cube_program); + CheckGLError("glUseProgram"); // Set the uniform values that will be used by our shader. glUniform3fv(_cube_position_uniform, 1, _cube_position); @@ -580,6 +601,7 @@ - (void)renderWithModelViewProjectionMatrix:(const float *)model_view_matrix { } else { glBindBuffer(GL_ARRAY_BUFFER, _cube_color_buffer); } + CheckGLError("glBindBuffer"); glVertexAttribPointer(_cube_color_attrib, 4, GL_FLOAT, GL_FALSE, sizeof(float) * 4, 0); glEnableVertexAttribArray(_cube_color_attrib); @@ -591,6 +613,7 @@ - (void)renderWithModelViewProjectionMatrix:(const float *)model_view_matrix { glDrawArrays(GL_TRIANGLES, 0, NUM_CUBE_VERTICES / 3); glDisableVertexAttribArray(_cube_vertex_attrib); glDisableVertexAttribArray(_cube_color_attrib); + CheckGLError("glDrawArrays"); // Select our shader. glUseProgram(_grid_program); @@ -616,34 +639,22 @@ - (void)renderWithModelViewProjectionMatrix:(const float *)model_view_matrix { glDisableVertexAttribArray(_grid_color_attrib); } -- (void)cardboardView:(GVRCardboardView *)cardboardView - didFireEvent:(GVRUserEvent)event { - switch (event) { - case kGVRUserEventBackButton: - NSLog(@"User pressed back button"); - break; - case kGVRUserEventTilt: - NSLog(@"User performed tilt action"); - break; - case kGVRUserEventTrigger: - NSLog(@"User performed trigger action"); - // Check whether the object is found. - if (_is_cube_focused) { - _success_source_id = [_gvr_audio_engine createStereoSound:kSuccessSoundFile]; - [_gvr_audio_engine playSound:_success_source_id loopingEnabled:false]; - // Vibrate the device on success. - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); - // Generate the next cube. - [self spawnCube]; - } - break; +- (void)handleTrigger { + NSLog(@"User performed trigger action"); + // Check whether the object is found. + if (_is_cube_focused) { + _success_source_id = [_gvr_audio_engine createStereoSound:kSuccessSoundFile]; + [_gvr_audio_engine playSound:_success_source_id loopingEnabled:false]; + // Vibrate the device on success. + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); + // Generate the next cube. + [self spawnCube]; } } -- (void)cardboardView:(GVRCardboardView *)cardboardView shouldPauseDrawing:(BOOL)pause { - if ([self.delegate respondsToSelector:@selector(shouldPauseRenderLoop:)]) { - [self.delegate shouldPauseRenderLoop:pause]; - } +- (void)pause:(BOOL)pause { + [super pause:pause]; + if (pause) { [_gvr_audio_engine pauseSound:_sound_object_id]; } else { diff --git a/Samples/TreasureHunt/TreasureHuntViewController.h b/Samples/TreasureHunt/TreasureHuntViewController.h index 2fc7e61..9cb9061 100644 --- a/Samples/TreasureHunt/TreasureHuntViewController.h +++ b/Samples/TreasureHunt/TreasureHuntViewController.h @@ -1,3 +1,19 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #import @interface TreasureHuntViewController : UIViewController diff --git a/Samples/TreasureHunt/TreasureHuntViewController.m b/Samples/TreasureHunt/TreasureHuntViewController.m index e219e0a..6aa51e5 100644 --- a/Samples/TreasureHunt/TreasureHuntViewController.m +++ b/Samples/TreasureHunt/TreasureHuntViewController.m @@ -1,66 +1,53 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #import "TreasureHuntViewController.h" -#import "TreasureHuntRenderLoop.h" #import "TreasureHuntRenderer.h" -@interface TreasureHuntViewController () { - GVRCardboardView *_cardboardView; - TreasureHuntRenderer *_treasureHuntRenderer; - TreasureHuntRenderLoop *_renderLoop; +#import + +@interface TreasureHuntViewController () { } @end @implementation TreasureHuntViewController -- (void)loadView { - _treasureHuntRenderer = [[TreasureHuntRenderer alloc] init]; - _treasureHuntRenderer.delegate = self; - - _cardboardView = [[GVRCardboardView alloc] initWithFrame:CGRectZero]; - _cardboardView.delegate = _treasureHuntRenderer; - _cardboardView.autoresizingMask = - UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - - _cardboardView.vrModeEnabled = YES; - - // Use double-tap gesture to toggle between VR and magic window mode. - UITapGestureRecognizer *doubleTapGesture = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapView:)]; - doubleTapGesture.numberOfTapsRequired = 2; - [_cardboardView addGestureRecognizer:doubleTapGesture]; - - self.view = _cardboardView; -} - -- (void)viewWillAppear:(BOOL)animated { - [super viewWillAppear:animated]; - - _renderLoop = [[TreasureHuntRenderLoop alloc] initWithRenderTarget:_cardboardView - selector:@selector(render)]; -} - -- (void)viewDidDisappear:(BOOL)animated { - [super viewDidDisappear:animated]; +- (void)viewDidLoad { + [super viewDidLoad]; - // Invalidate the render loop so that it removes the strong reference to cardboardView. - [_renderLoop invalidate]; - _renderLoop = nil; -} - -- (GVRCardboardView *)getCardboardView { - return _cardboardView; -} + self.view.backgroundColor = [UIColor whiteColor]; -#pragma mark - TreasureHuntRendererDelegate + TreasureHuntRenderer *renderer = [[TreasureHuntRenderer alloc] init]; -- (void)shouldPauseRenderLoop:(BOOL)pause { - _renderLoop.paused = pause; + // Embedded (widget) view with its own view controller. + GVRRendererViewController *viewController = + [[GVRRendererViewController alloc] initWithRenderer:renderer]; + viewController.delegate = self; + viewController.view.frame = CGRectMake(20, 50, self.view.bounds.size.width - 40, 200); + viewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth; + [self.view addSubview:viewController.view]; + [self addChildViewController:viewController]; } -#pragma mark - Implementation +#pragma mark - GVRRendererViewControllerDelegate -- (void)didDoubleTapView:(id)sender { - _cardboardView.vrModeEnabled = !_cardboardView.vrModeEnabled; +- (GVRRenderer *)rendererForDisplayMode:(GVRDisplayMode)displayMode { + // Always present (not push) view controller for fullscreen landscape right VR mode. + return [[TreasureHuntRenderer alloc] init]; } @end diff --git a/Samples/TreasureHuntNDK/TreasureHuntNDK-Info.plist b/Samples/TreasureHuntNDK/TreasureHuntNDK-Info.plist index 8e3f4cd..7a7a68b 100644 --- a/Samples/TreasureHuntNDK/TreasureHuntNDK-Info.plist +++ b/Samples/TreasureHuntNDK/TreasureHuntNDK-Info.plist @@ -11,7 +11,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - com.google.${PRODUCT_NAME:rfc1034identifier} + ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Samples/VideoWidgetDemo/Info.plist b/Samples/VideoWidgetDemo/Info.plist index 7adc08c..e5597fe 100644 --- a/Samples/VideoWidgetDemo/Info.plist +++ b/Samples/VideoWidgetDemo/Info.plist @@ -22,7 +22,7 @@ CFBundleIdentifier - com.google.${PRODUCT_NAME} + ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -45,7 +45,12 @@ UIMainStoryboardFile Main - NSCameraUsageDescription + NSCameraUsageDescription Used to scan QR codes + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + diff --git a/Samples/VideoWidgetDemo/Main.storyboard b/Samples/VideoWidgetDemo/Main.storyboard index e5e30a0..63b3793 100644 --- a/Samples/VideoWidgetDemo/Main.storyboard +++ b/Samples/VideoWidgetDemo/Main.storyboard @@ -1,8 +1,11 @@ - - + + + + + - - + + @@ -19,7 +22,6 @@ - @@ -34,7 +36,7 @@ - + + + + + + + + + + + + + - + - + + + + - - - - - - - - - - - + + + - + + - + + - - - + @@ -114,7 +123,7 @@ Gorillas' natural habitats cover tropical or subtropical forests in Africa. Alth - + @@ -128,12 +137,12 @@ Gorillas' natural habitats cover tropical or subtropical forests in Africa. Alth - + - + @@ -149,7 +158,25 @@ Gorillas' natural habitats cover tropical or subtropical forests in Africa. Alth - + + + + + + + + + + + + + + + + + + + diff --git a/Samples/VideoWidgetDemo/Podfile b/Samples/VideoWidgetDemo/Podfile index bf77d03..9b3bfb8 100644 --- a/Samples/VideoWidgetDemo/Podfile +++ b/Samples/VideoWidgetDemo/Podfile @@ -1,3 +1,3 @@ target 'VideoWidgetDemo' do - pod 'GVRSDK' + pod 'GVRKit' end diff --git a/Samples/VideoWidgetDemo/VideoPlayerViewController.h b/Samples/VideoWidgetDemo/VideoPlayerViewController.h index 09f3a42..65185f2 100644 --- a/Samples/VideoWidgetDemo/VideoPlayerViewController.h +++ b/Samples/VideoWidgetDemo/VideoPlayerViewController.h @@ -1,4 +1,5 @@ #import @interface VideoPlayerViewController : UIViewController + @end diff --git a/Samples/VideoWidgetDemo/VideoPlayerViewController.m b/Samples/VideoWidgetDemo/VideoPlayerViewController.m index ee32d35..4d116b8 100644 --- a/Samples/VideoWidgetDemo/VideoPlayerViewController.m +++ b/Samples/VideoWidgetDemo/VideoPlayerViewController.m @@ -1,22 +1,19 @@ #import +#import #import "VideoPlayerViewController.h" -#import "GVRVideoView.h" - -@interface VideoPlayerViewController () -@property(nonatomic) IBOutlet GVRVideoView *videoView; +@interface VideoPlayerViewController () +@property(nonatomic) IBOutlet GVRRendererView *videoView; @property(nonatomic) IBOutlet UITextView *attributionTextView; +@property(nonatomic) IBOutlet UIToolbar *toolbar; +@property(nonatomic) UIBarButtonItem *playButton; +@property(nonatomic) UIBarButtonItem *pauseButton; +@property(nonatomic) UIBarButtonItem *progressBar; +@property(nonatomic) AVPlayer *player; @end -@implementation VideoPlayerViewController { - BOOL _isPaused; -} - -- (instancetype)init { - self = [super initWithNibName:nil bundle:nil]; - return self; -} +@implementation VideoPlayerViewController - (void)viewDidLoad { [super viewDidLoad]; @@ -32,53 +29,127 @@ - (void)viewDidLoad { _attributionTextView.attributedText = attributedText; - _videoView.delegate = self; - _videoView.enableFullscreenButton = YES; - _videoView.enableCardboardButton = YES; - _videoView.enableTouchTracking = YES; - - _isPaused = YES; + // Setup toolbar buttons. + _playButton = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay + target:self + action:@selector(updateVideoPlayback)]; + _pauseButton = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPause + target:self + action:@selector(updateVideoPlayback)]; + UIProgressView *progressView = + [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; + [progressView sizeToFit]; + _progressBar = [[UIBarButtonItem alloc] initWithCustomView:progressView]; // Load the sample 360 video, which is of type stereo-over-under. NSString *videoPath = [[NSBundle mainBundle] pathForResource:@"congo" ofType:@"mp4"]; - [_videoView loadFromUrl:[[NSURL alloc] initFileURLWithPath:videoPath] - ofType:kGVRVideoTypeStereoOverUnder]; - + NSURL *videoURL = [[NSURL alloc] initFileURLWithPath:videoPath]; // Alternatively, this is how to load a video from a URL: - //NSURL *videoURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/googlevr/gvr-ios-sdk" - // @"/master/Samples/VideoWidgetDemo/resources/congo.mp4"]; - //[_videoView loadFromUrl:videoURL ofType:kGVRVideoTypeStereoOverUnder]; - + // NSURL *videoURL = [NSURL + // URLWithString:@"https://raw.githubusercontent.com/googlevr/gvr-ios-sdk" + // @"/master/Samples/VideoWidgetDemo/resources/congo.mp4"]; + + _player = [AVPlayer playerWithURL:videoURL]; + _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playerItemDidReachEnd:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:[_player currentItem]]; + __weak __typeof(self) weakSelf = self; + [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC) + queue:NULL + usingBlock:^(CMTime time) { [weakSelf updateProgressBar]; }]; + + GVRRendererViewController *viewController = self.childViewControllers[0]; + GVRSceneRenderer *sceneRenderer = (GVRSceneRenderer *)viewController.rendererView.renderer; + GVRVideoRenderer *videoRenderer = [sceneRenderer.renderList objectAtIndex:0]; + videoRenderer.player = _player; + _videoView = (GVRRendererView *)viewController.view; } -#pragma mark - GVRVideoViewDelegate +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; -- (void)widgetViewDidTap:(GVRWidgetView *)widgetView { - if (_isPaused) { - [_videoView play]; - } else { - [_videoView pause]; + [self updateVideoPlayback]; +} + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + [super prepareForSegue:segue sender:sender]; + if ([segue.destinationViewController isKindOfClass:[GVRRendererViewController class]]) { + GVRRendererViewController *viewController = segue.destinationViewController; + viewController.delegate = self; } - _isPaused = !_isPaused; } -- (void)widgetView:(GVRWidgetView *)widgetView didLoadContent:(id)content { - NSLog(@"Finished loading video"); - [_videoView play]; - _isPaused = NO; +#pragma mark - Actions + +- (IBAction)didTapPlayPause:(id)sender { + [self updateVideoPlayback]; +} + +- (void)updateProgressBar { + UIProgressView *progressView = (UIProgressView *)_progressBar.customView; + double duration = CMTimeGetSeconds(_player.currentItem.duration); + double time = CMTimeGetSeconds(_player.currentTime); + progressView.progress = (CGFloat)(time / duration); } -- (void)widgetView:(GVRWidgetView *)widgetView - didFailToLoadContent:(id)content - withErrorMessage:(NSString *)errorMessage { - NSLog(@"Failed to load video: %@", errorMessage); +#pragma mark - AVPlayer + +- (void)playerItemDidReachEnd:(NSNotification *)notification { + AVPlayerItem *player = [notification object]; + [player seekToTime:kCMTimeZero]; +} + +#pragma mark - GVRRendererViewControllerDelegate + +- (void)didTapTriggerButton { + [self updateVideoPlayback]; } -- (void)videoView:(GVRVideoView*)videoView didUpdatePosition:(NSTimeInterval)position { - // Loop the video when it reaches the end. - if (position == videoView.duration) { - [_videoView seekTo:0]; - [_videoView play]; +- (GVRRenderer *)rendererForDisplayMode:(GVRDisplayMode)displayMode { + GVRVideoRenderer *videoRenderer = [[GVRVideoRenderer alloc] init]; + videoRenderer.player = _player; + [videoRenderer setSphericalMeshOfRadius:50 + latitudes:12 + longitudes:24 + verticalFov:180 + horizontalFov:360 + meshType:kGVRMeshTypeStereoTopBottom]; + + GVRSceneRenderer *sceneRenderer = [[GVRSceneRenderer alloc] init]; + [sceneRenderer.renderList addRenderObject:videoRenderer]; + + if (displayMode == kGVRDisplayModeEmbedded) { + // Hide the reticle in embedded mode. + sceneRenderer.hidesReticle = YES; + } else { + // In fullscreen mode, add the toolbar to the GL scene. + GVRUIViewRenderer *viewRenderer = [[GVRUIViewRenderer alloc] initWithView:_toolbar]; + + // Position the playback controls half a meter in front (z = -0.5). + GLKMatrix4 position = GLKMatrix4MakeTranslation(-0.0, -0.3, -0.5); + // Rotate along x axis so that it looks oriented towards us. + position = GLKMatrix4RotateX(position, GLKMathDegreesToRadians(-20)); + viewRenderer.position = position; + + [sceneRenderer.renderList addRenderObject:viewRenderer]; + } + + return sceneRenderer; +} + +#pragma mark - Private + +- (void)updateVideoPlayback { + if (_player.rate == 1.0) { + [_player pause]; + _toolbar.items = @[ _playButton, _progressBar ]; + } else { + [_player play]; + _toolbar.items = @[ _pauseButton, _progressBar ]; } }