[swift] SceneKit uv animation
프로퍼티
쉐이더에 전달할 구조체를 정의한다.
앱에서 사용할 구조체와 쉐이더에서 입력받을 구조체 각각 정의.
delta는 uv 변화량을 나타내며, type은 애니메이션 방향이다.
// swift
struct MyInputValue {
var delta: Float = 0.0
var type: UInt8 = 0
init(delta: Float, type: UInt8) {
self.delta = delta
self.type = type
}
}
// c header
#include <metal_stdlib>
#include <SceneKit/scn_metal>
using anmespace metal;
struct MyInputValue {
float delta;
uint8_t type;
};
텍스처 이미지 설정
uv 에니메이션에 사용할 이미지를 SCNMaterialProperty 로 생성해 매터리얼에 전달한다.
// iamge for fragment shader
#if os(iOS)
if let image = UIImage(named: "CustomImage") {
let imageProperty = SCNMaterialProperty(contents: image)
material.setValue(imageProperty, forKey: "dataTexture")
}
#else
if let image = NSImage(named: "CustomImage") {
let imageProperty = SCNMaterialProperty(contents: image)
material.setValue(imageProperty, forKey: "dataTexture")
}
#endif
프로퍼티 전달
1회성으로 초기에 프로퍼티를 전달
쉐이더에 한번 데이터를 전달하는 경우 매터리얼의 setValue 함수를 사용해 데이터를 전달한다.
setValue에 두번재 인자 forKey는 쉐이더 함수의 인자 이름과 같아야 한다.
// property for vertex shader
var values = MyInputValue(delta: 0)
// Data 형식으로 변환
let data = Data(bytes: &values, count: MemoryLayout<MyInputValue>.stride)
// 쉐이더 인자 중 value 에 설정
material.setValue(data, forKey:"value")
// 텍스처이미지를 쉐이더 인자 중 dataTexture 에 설정
material.setValue(property, forKey:"dataTexture")
동적 프로퍼티 전달
매 프레임별로 변경된 사용자 데이터를 전달하는 경우 프로그램의 바인딩 핸들러를 사용하고, 핸들러 인자인 bufferStream의 writeBytes 메쏘드로 데이터를 전달한다.
쉐이더에 uv 값을 전달해 uv를 조정하는 경우 기존 렌더파이프라인의 변환과 관련없이 처리되므로, 기존 반영한 uv 값이 유지되지 않는다.(항상 기본 uv 값으로 설정됨)
쉐이더에서는 uv값이 초기값으로 호출되므로 쉐이더에서 이전 프레임과의 변화를 계산하기 어려움이 있다.
swift에서 이전 변화량을 저장한 뒤 누적해서 계산한 뒤에 데이터를 전달해야 한다.
변화된 uv 값 누적을 위해 별도 전역변수 추가.
uv 값은 0.0~1.0 값을 가지는데, 1초에 uv를 얼만큼 이동할지에 따라 값이 달라진다.
이전프레임과이 시간차이만큼 uv 값을 더해주면 자연스럽게 이동하게 된다.
아래 예에서는 1초게 2번 uv를 회전하는 것이라 2를 곱함.
동적으로 속도 조절을 위해 speedRatio 변수 추가.
// 이전 값들과 비교를 위한 전역변수
var speedRatio: Float = 0
var uvValue: CFTimeInterval = 0.0
var prevTime: CFTimeInterval = 0.0
// 쉐이더 동작시 호출되는 핸들러 메쏘드
program.handleBinding(
ofBufferNamed: "value",
frequency: .perNode,
handler: { [weak self](bufferStream, node, shadable, renderer) in
guard let self else { return }
// 시간값에 따른 uv 변화량을 계산하기 위해 현재 시각 얻기
let time = CACurrentMediaTime()
if prevTime == 0 {
prevTime = time
}
// 이전 프레임과의 경과 시간 계산
var elapse:CFTimeInterval = 0
elapse = time - prevTime
prevTime = time
// uv 변화량 계산
// 속도변수 speedRatio(0.0~1.0)에 따라 적절한 uv 값 생성
// 속도가 너무 느린경우 임의 값을 곱해 속도 늘리기
uvValue += (elapse * Double(self.speedRatio) * 2)
// 수치가 너무 커지면 초기화
if uvValue > 10.0 {
uvValue = 0
}
// 쉐이더에 값 전달을 위해 해당 구조체로 데이터 생성
var values = MyInputValue(delta: Float(uvValue), type: 0)
// 쉐이더에 전달
bufferStream.writeBytes(&values, count: MemoryLayout<MyInputValue>.stride)
}
위 핸들러에서 uvVlaue와 speedRatio 값을 쉐이더에 전달하므로, ui에서 speedRatio 값을 변경하면 동적으로 속도가 변경된다.
func setSpeed(value: Float) {
speedRatio = value
if speedRatio > 1.0 {
speedRatio = 1.0
} else if speedRatio < -1.0 {
speedRatio = -1.0
}
}
shader
쉐이더에서 사용하는 구조체 정의.
메탈쉐이더에서 사전 정의된 요소들이라 사용하는 어트리뷰트들을 포함해 구조체를 정의한다.
Metal , Scenekit에서는 사용하는 어트리뷰트와 SCNSceneBuffer과 사전 정의되어 있으므로, 버텍스 입력 데이터의 어트리뷰트 접근시에는 해당 인덱스를 사용해야 하며, buffer(0)은 SCNSceneBuffer, buffer(1)은 노드 데이터가 전달된다. 해당 인덱스와 SCNSceneBuffer 구조체는 아래 레퍼런스 참조.
SCNProgram | Apple Developer Documentation
SCNProgram | Apple Developer Documentation
A complete Metal or OpenGL shader program that replaces SceneKit's rendering of a geometry or material.
developer.apple.com
각 데이터 구조체를 정의한다.
// 버텍스 쉐이더 입력 데이터
typedef struct {
float3 position [[ attribute(SCNVertexSemanticPosition) ]];
float2 texCoords [[ attribute(SCNVertexSemanticTexcoord0) ]];
} VertexInput;
// 프레그먼트 쉐이더 입력 데이터
typedef struct
{
float4 position [[position]];
float2 texCoords;
} FragmentInput;
// 노드 데이터 : 원하는 데이터만 정의해 사용
struct MyNodeBuffer {
float4x4 modelTransform;
// float4x4 inverseModelTransform;
float4x4 modelViewTransform;
// float4x4 inverseModelViewTransform;
float4x4 normalTransform;
float4x4 modelViewProjectionTransform;
// float4x4 inverseModelViewProjectionTransform;
// float2x3 boundingBox;
// float2x3 worldBoundingBox;
};
vertex shader
buffer(0), buffer(1)은 SceneKit에서 정의된 데이터이고, buffer(2) 부터 커스텀 데이터를 위한 버퍼로 사용된다.
buffer(2) 에 전달된 입력값을 MyInputValue 구조체에 할당.
전달된 값을 기반으로 uv 좌표를 조절해 준다. type 값에 따라 x방향, y방향 애니메이션 방향 설정되도록 구성.
vertex FragmentInput myVertex(VertexInput in [[ stage_in ]],
constant SCNSceneBuffer& scn_frame [[buffer(0)]],
constant MyNodeBuffer& scn_node [[buffer(1)]],
constant MyInputValue& value [[buffer(2)]]
)
{
FragmentInput vert;
vert.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);
if (value.value == 0) {
vert.texCoords = in.texCoords;
} else {
// 1초에 uv를 얼마나 움직일지 미리 정의 필요, 도로 메시의 길이에 따라 속도 고려
// 실제속도 혹은 특정속도 이상으로 이동 시키는 경우 착시현상(스트로보효과) 발생
// 속도에 따라 역방향으로 보여지므로 인식할 수 있는 수준의 속도로 줄여서 표현
if (value.type == 0) {
vert.texCoords = float2(in.texCoords.x, in.texCoords.y + (value.delta ));
} else {
vert.texCoords = float2(in.texCoords.x + (value.delta), in.texCoords.y);
}
}
return vert;
}
fragment shader
uv 애니메이션에서 fragment 쉐이더는 특별히 수정할 일이 없으니, 텍스처 정보만 반영해 준다.
fragment float4 myFragment(FragmentInput in [[ stage_in ]],
texture2d<float, access::sample> dataTexture [[ texture(0) ]]
)
{
constexpr sampler sampler2d(coord::normalized, filter::linear, address::repeat);
float4 color = dataTexture.sample(sampler2d, in.texCoords);
return color;
}
Shader Modifier
SceneKit 에서는 위 처럼 완전히 쉐이더를 대체하는 방법 외에 특정 쉐이더 처리만 끼워(?) 넣는 방식도 존재한다.
쉐이더를 대체하면 각종 라이트, 재질 등 기본적으로 Scenekit에서 처리해 주었던 환경도 직접 구현해야 한다.
uv 이동과 같은 간단한 쉐이더 처리는 아래 방식이 더 간편하고, 기존 설정된 쉐이더 위에서 동작하기에 라이트나 재질 등 유지됨.
단, 프레임별 핸들러 등이 따로 없어서 동적으로 변경되는 값을 전달하려면 별도 에니메이션 처리등을 구현 후 호출해줘야 한다.
/// scenekit shader modifier
let shader = """
uniform float value;
uniform float u_time;
_geometry.texcoords[0] = vec2(
_geometry.texcoords[0].x,
(_geometry.texcoords[0].y + value);
_geometry.texcoords[1] = vec2(
_geometry.texcoords[1].x,
(_geometry.texcoords[1].y + value);
"""
material.setValue(value, forKey: "value")
material.shaderModifiers = [ SCNShaderModifierEntryPoint.geometry: shader ]