Sunday 15 December 2013

UnityPlayer as Subview with Transparent Background (iOS)

In a previous post I was experimenting with Android & Unity to be able to use Unity as subview overlaying a native UI made with Android... now I tried to do the same but with iOS.

Since Unity 4.2 it became easier to override unity ios view to do your own custom one and combine it with native views inside iOS application.

In this post I will outline the steps to have a simple unity scene with native controls using Unity 4.2 (basic license).

Unity Scene
  1. For simplicity I created a scene with a rotating cube and for the camera I set the color to (0,0,0,0) 
  2. Inside one of my "RotatingCube" gameobject's scripts I added a listener function that simply toggle the direction everytime it is called:
  3. void OnTogglePressed(string message)
    {
         Debug.Log("Toggle direction");
         direction *= -1;
    } 
    
  4. Once all done save the scene and build for iOS to generate XCode project


The generated XCode project
  1. First thing to do is to support transparent background in unity view. To do that simply do a minor change to CreateSystemRenderingSurface method inside GlesHelper.mm to have:
    surface->layer.opaque = NO;
  2. Create UIViewController (you can also subclass UnityViewControllerBase if you wish) with some native controls: In my case I simply added a UILabel and UIButton (which I will use to toggle rotation direction) 
  3. Create a subclass for UnityAppController (e.g. MyUnityAppController) where we will override some methods:
  4.  
    // the header file
    #import "UnityView.h"
    #import "UnityAppController.h"
    @interface MyUnityAppController : UnityAppController
    @end
    
    // the implementation file
    #import "MyUnityAppController.h"
    #import "MyNativeViewController.h"
    @interface MyUnityAppController ()
    @end
    @implementation MyUnityAppController
    // I simply removed the user interaction from unityview
    - (UnityView*)initUnityViewImpl
    {
        UnityView *unityView = [[UnityView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        unityView.userInteractionEnabled = NO;
        return unityView;
    }
    // here I am using my UIViewController as _rootController
    - (void)createViewHierarchyImpl
    {
     _rootView = _unityView;
     _rootController = [[MyNativeViewController alloc] init];
    }
    // I had to override this method (although unity doesn't recommend it) to change one line 
    // instead of assigning _rootView as _rootViewController.view I've added it as a subview
    - (void)createViewHierarchy
    {
     [self createViewHierarchyImpl];
     NSAssert(_rootView != nil, @"createViewHierarchyImpl must assign _rootView");
     NSAssert(_rootController != nil, @"createViewHierarchyImpl must assign _rootController");
        
     [_rootController.view addSubview:_rootView];
     if([_rootController isKindOfClass: [UnityViewControllerBase class]])
      [(UnityViewControllerBase*)_rootController assignUnityView:_unityView];
    }
    @end
    
  5. Inside my UIViewController I set the UIButton to call "OnTogglePressed" method mentioned earlier when building the scene using UnitySendMessage method:
  6.  
    -(IBAction)togglePressed
    {
        UnitySendMessage("RotatingCube", "OnTogglePressed", "");
    }
    
  7. Last thing to do is to change the appcontroller name inside main.mm from UnityAppController to MyUnityAppController then you should be able to see the cube scene with the label and the button:
Video of it in action:



There are few things which need to be investigated: for example whether we can have only a part of the screen for Unity Scene (similar to what I've done with Android). Setting unityview frame didn't give me expected results so I might need to dig deeper.
For now this seems to work using full screen.

If anyone have good ideas or did something similar please share! :-)

13 comments:

  1. Excellent post Yaser.

    Cheers.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This is awesome! Thank you for sharing. Did you happen to dig a bit deeper on making the Unity Scene be something smaller than full screen? I also tried out messing with the the unityview frame but didn't get what I needed.

    ReplyDelete
    Replies
    1. Thanks Fernando.

      Didn't have the time or the chance yet.

      Delete
  4. Sadly, this doesn't seem to work for Unity 5.2. Any idea how I can accomplish this?

    ReplyDelete
  5. On iOS with Metal API you have to change the file MetalHelper.mm to:

    CGFloat backgroundColor[] = {0,0,0,0};

    and add

    surface->layer.opaque = NO;

    Works for me on Uniti 5.2.

    ReplyDelete
    Replies
    1. Thanks for that. I didnt try with Unity 5.2 yet

      Delete
  6. What are your settings in Unity, Are you sure it worked for you?

    ReplyDelete
  7. Of course I am sure :)
    Maybe this snippet helps you.
    Does your iOS app runs unity with Metal and not with a fault back to OpenGL?

    extern "C" void CreateSystemRenderingSurfaceMTL(UnityDisplaySurfaceMTL* surface)
    {
    DestroySystemRenderingSurfaceMTL(surface);

    MTLPixelFormat colorFormat = surface->srgb ? MTLPixelFormatBGRA8Unorm_sRGB : MTLPixelFormatBGRA8Unorm;

    surface->layer.presentsWithTransaction = NO;
    surface->layer.drawsAsynchronously = YES;
    CGFloat backgroundColor[] = {0,0,0,0};
    surface->layer.opaque = NO;
    surface->layer.backgroundColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), backgroundColor);
    surface->layer.device = surface->device;
    surface->layer.pixelFormat = colorFormat;
    //surface->layer.framebufferOnly = YES;
    surface->layer.framebufferOnly = NO;
    surface->colorFormat = colorFormat;
    }

    ReplyDelete
  8. It works on Unity 2017.1.1f1 but it doesn't make it completely transparent. For example if you set native view background as dark blue and place the unity view as a subview on top of native view. The area of unity subview have a background like light blue. How can it be fixed?

    ReplyDelete
  9. extern "C" void CreateSystemRenderingSurfaceMTL(UnityDisplaySurfaceMTL* surface)
    {
    DestroySystemRenderingSurfaceMTL(surface);

    MTLPixelFormat colorFormat = GetColorFormatForSurface(surface);
    surface->layer.presentsWithTransaction = NO;
    surface->layer.drawsAsynchronously = YES;

    #if !PLATFORM_OSX
    if (UnityPreserveFramebufferAlpha())
    {
    const CGFloat components[] = {1.0f, 1.0f, 1.0f, 0.0f};
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGColorRef color = CGColorCreate(colorSpace, components);
    surface->layer.opaque = NO;
    CGFloat backgroundColor[] = {0,0,0,0};
    surface->layer.backgroundColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), backgroundColor);
    // surface->layer.backgroundColor = color;
    CGColorRelease(color);
    CGColorSpaceRelease(colorSpace);
    }
    else
    #endif
    {
    surface->layer.opaque = NO;
    }

    #if PLATFORM_OSX
    MetalUpdateDisplaySync();
    #endif


    #if PLATFORM_OSX
    CGColorSpaceRef colorSpaceRef = nil;
    if (surface->wideColor)
    colorSpaceRef = CGColorSpaceCreateWithName(surface->srgb ? kCGColorSpaceExtendedLinearSRGB : kCGColorSpaceExtendedSRGB);
    else
    colorSpaceRef = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);

    surface->layer.colorspace = colorSpaceRef;
    CGColorSpaceRelease(colorSpaceRef);
    #endif

    surface->layer.device = surface->device;
    surface->layer.pixelFormat = colorFormat;
    surface->layer.framebufferOnly = NO;//(surface->framebufferOnly != 0);
    surface->colorFormat = colorFormat;

    MTLTextureDescriptor* txDesc = [MTLTextureDescriptorClass texture2DDescriptorWithPixelFormat: colorFormat width: surface->systemW height: surface->systemH mipmapped: NO];
    #if PLATFORM_OSX
    txDesc.resourceOptions = MTLResourceCPUCacheModeDefaultCache | MTLResourceStorageModeManaged;
    #endif
    txDesc.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;

    @synchronized(surface->layer)
    {
    OSAtomicCompareAndSwap32Barrier(surface->bufferChanged, 0, &surface->bufferChanged);

    for (int i = 0; i < kUnityNumOffscreenSurfaces; i++)
    {
    OSAtomicCompareAndSwap32Barrier(surface->bufferCompleted[i], -1, &surface->bufferCompleted[i]);
    // Allocating a proxy texture is cheap until it's being rendered to and the GPU driver does allocation
    surface->drawableProxyRT[i] = [surface->device newTextureWithDescriptor: txDesc];
    }

    #if PLATFORM_OSX
    OSAtomicCompareAndSwap32Barrier(surface->writeCount, surface->writeCount + (kUnityNumOffscreenSurfaces - 1), &surface->writeCount);
    OSAtomicCompareAndSwap32Barrier(surface->readCount, surface->writeCount - 1, &surface->readCount);
    #endif
    }
    }

    This is the method that we get after exporting the project from the unity. Still the background does not changes in unity 2018.3.0f2 version. can anybody help in acheiving the unity with transparent background. Also I had tried to change the gleshelper.mm file also but result is same.

    ReplyDelete
    Replies
    1. Did you have any luck figuring this out? I can't get it to be fully transparent.

      Delete
  10. If you had been on the lookout for a on line casino that may settle for digital cash, then mBit is a superior alternative. Their intention to construct a crypto on line casino to meet all gamers' wants is a fantastic thought. Roulette wheels comprise 37 numbers from 0-37, with gamers place to} guess on single numbers, purple or black, odd and even tons of|and a lot of} other choices. Roulette is totally as much as} probability, with sure methods more useful than others. After a prolonged wait, Pennsylvania joined the web on line casino get together in 2019 with 메리트카지노 just two online on line casino choices. In relatively brief order, nevertheless, there were greater than a dozen totally different apps available in the Keystone State.

    ReplyDelete