Machineboy空

RealityKit - Plane에 벡터이미지 texture 적용하기, UnlitMaterial, opacityThreshold 본문

언어/iOS

RealityKit - Plane에 벡터이미지 texture 적용하기, UnlitMaterial, opacityThreshold

안녕도라 2024. 11. 12. 21:47

누끼 딴 사진을 3D 공간에 넣어 제스처를 적용하고 싶다!

  • image로 texture만드는 방법
  • texture를 투명하게 설정하는 방법 등을 소개할 예정이다!

Plane을 스티커처럼 이용하는 방법!


2D 인듯 3D인 Plane의 특성

Plane 축을 잘 생각하며 코드를 짜야 한다!

기존 translation의 경우 좌우가 y축, 위아래가 z축과 연동되어 있기 때문에

Plane 생성 시, width와 depth 활용!

let planeMesh = MeshResource.generatePlane(width: 1, depth: 1)

 


UnlitMaterial의 Texture 속성 설정

  • UnlitMaterial : A material that doesn’t respond to lights in the scene.
  • material.color = .init(tint:.white)
    • 정확히 기억은 안나는데 white가 0, black이 1 같은 그래픽스 개념이 있었는데 기억이 안난다.. 
    • 여튼 .black으로 바꾸면 이미지가 나타나지 않음.
  • material.opacityThreshold : 이걸 적용해 줘야 벡터 이미지 즉 투명 배경이 적용됌.

if let texture = try? TextureResource.load(named: imgName) {
                var material = UnlitMaterial()
                material.color = .init(tint: .white, texture: .init(texture))
                material.opacityThreshold = 0.1
                plane.model?.materials = [material]
            }

opacityThreshold 값을 적용한 것과 하지 않은 것

 

샘플 코드

import SwiftUI
import ARKit
import RealityKit

struct ContentView: View {
    @State private var imgName: String = ""
    
    var body: some View {
        VStack {
            ARViewContainer(imgName: $imgName)
                .ignoresSafeArea()
            
            HStack(spacing: 10) {
                ForEach(0..<4) { index in
                    Button(action: {
                        imgName = "pik\(index+1)"
                    }) {
                        Image("pik\(index+1)")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 80, height: 80)
                            .background(Color.teal)
                            .cornerRadius(10)
                    }
                }
            }
            
            HStack(spacing: 10) {
                ForEach(4..<7) { index in
                    Button(action: {
                        imgName = "pik\(index+1)"
                    }) {
                        Image("pik\(index+1)")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 80, height: 80)
                            .background(Color.teal)
                            .cornerRadius(10)
                    }
                }
            }
            
        }
    }
    
    struct ARViewContainer: UIViewRepresentable {
        @Binding var imgName: String
        
        func makeUIView(context: Context) -> ARView {
            let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
            arView.environment.background = .color(.white)
            
            let emptyAnchor = AnchorEntity(world: [0, 0, 0])
            context.coordinator.emptyAnchor = emptyAnchor
            arView.scene.addAnchor(emptyAnchor)
            

            let camera = PerspectiveCamera()
            camera.position = [0, 5, 0]
            camera.look(at: emptyAnchor.position, from: camera.position, relativeTo: nil)
            
            let cameraAnchor = AnchorEntity(world: [0, 0, 0])
            cameraAnchor.addChild(camera)
            arView.scene.addAnchor(cameraAnchor)
            
            context.coordinator.arView = arView
            return arView
        }
        
        func updateUIView(_ uiView: ARView, context: Context) {
            context.coordinator.addDecoEntity(imgName: imgName)
        }
        
        func makeCoordinator() -> Coordinator {
            return Coordinator()
        }
    }
    
    class Coordinator: NSObject {
        var arView: ARView?
        var emptyAnchor: AnchorEntity?
        
        func addDecoEntity(imgName: String) {
            guard !imgName.isEmpty, let arView = arView else { return }
            
            let planeMesh = MeshResource.generatePlane(width: 1, depth: 1)
            let plane = ModelEntity(mesh: planeMesh)
            
            if let texture = try? TextureResource.load(named: imgName) {
                var material = UnlitMaterial()
                material.color = .init(tint: .white, texture: .init(texture))
                material.opacityThreshold = 0.1
                plane.model?.materials = [material]
            }
            
            plane.generateCollisionShapes(recursive: true)
            arView.installGestures([.all], for: plane)
            
            emptyAnchor?.addChild(plane)
            
        }
    }
}



#Preview {
    ContentView()
}

 

imgName값이 바뀌어야 updateUIView함수가 호출되기 때문에 동일한 버튼을 두 번 선택할 경우, 이미지가 두 개 생성되지 않는 문제가 있다.. 고쳐나갈 것!


+ 별 짓을 하다가 겨우 성공한 코드)

추후 변수명 수정 예정..

import SwiftUI
import ARKit
import RealityKit
import Combine

class CurrentImage: ObservableObject {
    @Published var imgName: String = ""
}

struct ContentView: View {
    @ObservedObject var currentImage = CurrentImage()

    var body: some View {
        VStack {
            ARViewContainer(currentImage: currentImage)
                .ignoresSafeArea()

            HStack(spacing: 10) {
                ForEach(0..<4) { index in
                    Button(action: {
                        currentImage.imgName = "pik\(index+1)"
                    }) {
                        Image("pik\(index+1)")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 80, height: 80)
                            .background(Color.teal)
                            .cornerRadius(10)
                    }
                }
            }

            HStack(spacing: 10) {
                ForEach(4..<7) { index in
                    Button(action: {
                        currentImage.imgName = "pik\(index+1)"
                    }) {
                        Image("pik\(index+1)")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 80, height: 80)
                            .background(Color.teal)
                            .cornerRadius(10)
                    }
                }
            }
        }
    }

    struct ARViewContainer: UIViewRepresentable {
        @ObservedObject var currentImage: CurrentImage

        func makeUIView(context: Context) -> ARView {
            let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
            arView.environment.background = .color(.white)

            let emptyAnchor = AnchorEntity(world: [0, 0, 0])
            context.coordinator.emptyAnchor = emptyAnchor
            arView.scene.addAnchor(emptyAnchor)

            let camera = PerspectiveCamera()
            camera.position = [0, 5, 0]
            camera.look(at: emptyAnchor.position, from: camera.position, relativeTo: nil)

            let cameraAnchor = AnchorEntity(world: [0, 0, 0])
            cameraAnchor.addChild(camera)
            arView.scene.addAnchor(cameraAnchor)

            context.coordinator.arView = arView
            return arView
        }

        func updateUIView(_ uiView: ARView, context: Context) {
        }

        func makeCoordinator() -> Coordinator {
            return Coordinator(currentImage: currentImage)
        }
    }

    class Coordinator: NSObject {
        var arView: ARView?
        var emptyAnchor: AnchorEntity?
        var cancellable: AnyCancellable?
        var currentImage: CurrentImage

        init(currentImage: CurrentImage) {
            self.currentImage = currentImage
            super.init()
            addDecoAndClear()
        }

        func addDecoAndClear() {
            cancellable = currentImage.$imgName
                .filter { !$0.isEmpty }
                .sink { [weak self] imgName in
                    self?.addDecoEntity(imgName: imgName)
                    self?.currentImage.imgName = ""
                }
        }

        func addDecoEntity(imgName: String) {
            guard !imgName.isEmpty, let arView = arView else { return }

            let planeMesh = MeshResource.generatePlane(width: 1, depth: 1)
            let plane = ModelEntity(mesh: planeMesh)

            if let texture = try? TextureResource.load(named: imgName) {
                var material = UnlitMaterial()
                material.color = .init(tint: .white, texture: .init(texture))
                material.opacityThreshold = 0.1
                plane.model?.materials = [material]
            }

            plane.generateCollisionShapes(recursive: true)
            arView.installGestures([.all], for: plane)

            emptyAnchor?.addChild(plane)
        }
    }
}

#Preview {
    ContentView()
}

 

[결론]역시나 쉽게 해결될 줄 알았다... Coordinator자체를 Observable로 만드는 방법

import SwiftUI
import ARKit
import RealityKit

struct ContentView: View {
    @StateObject private var coordinator = Coordinator() // Use Coordinator as an ObservableObject
    
    var body: some View {
        VStack {
            ARViewContainer(coordinator: coordinator)
                .ignoresSafeArea()
            
            HStack(spacing: 10) {
                ForEach(0..<4) { index in
                    Button(action: {
                        coordinator.addDecoEntity(imgName: "pik\(index+1)")
                    }) {
                        Image("pik\(index+1)")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 80, height: 80)
                            .background(Color.teal)
                            .cornerRadius(10)
                    }
                }
            }
            
            HStack(spacing: 10) {
                ForEach(4..<7) { index in
                    Button(action: {
                        coordinator.addDecoEntity(imgName: "pik\(index+1)")
                    }) {
                        Image("pik\(index+1)")
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 80, height: 80)
                            .background(Color.teal)
                            .cornerRadius(10)
                    }
                }
            }
        }
    }

    struct ARViewContainer: UIViewRepresentable {
        @ObservedObject var coordinator: Coordinator

        func makeUIView(context: Context) -> ARView {
            let arView = ARView(frame: .zero, cameraMode: .nonAR, automaticallyConfigureSession: false)
            arView.environment.background = .color(.white)

            let emptyAnchor = AnchorEntity(world: [0, 0, 0])
            coordinator.emptyAnchor = emptyAnchor
            arView.scene.addAnchor(emptyAnchor)

            let camera = PerspectiveCamera()
            camera.position = [0, 5, 0]
            camera.look(at: emptyAnchor.position, from: camera.position, relativeTo: nil)

            let cameraAnchor = AnchorEntity(world: [0, 0, 0])
            cameraAnchor.addChild(camera)
            arView.scene.addAnchor(cameraAnchor)

            coordinator.arView = arView
            return arView
        }

        func updateUIView(_ uiView: ARView, context: Context) {}
        
        func makeCoordinator() -> Coordinator {
            return coordinator
        }
    }

    class Coordinator: NSObject, ObservableObject {
        var arView: ARView?
        var emptyAnchor: AnchorEntity?
        
        func addDecoEntity(imgName: String) {
            guard !imgName.isEmpty, let arView = arView else { return }

            let planeMesh = MeshResource.generatePlane(width: 1, depth: 1)
            let plane = ModelEntity(mesh: planeMesh)

            if let texture = try? TextureResource.load(named: imgName) {
                var material = UnlitMaterial()
                material.color = .init(tint: .white, texture: .init(texture))
                material.opacityThreshold = 0.1
                plane.model?.materials = [material]
            }

            plane.generateCollisionShapes(recursive: true)
            arView.installGestures([.all], for: plane)

            emptyAnchor?.addChild(plane)
        }
    }
}

#Preview {
    ContentView()
}