RealityKitのCustomMaterialでアウトラインを描くShader
RealityKitのCustomMaterialでアウトラインを描くShaderを作ってみました
オマケでゲーミングアウトラインモードもあります
RealityKitでアウトライン付けるのできた!
— ふじき (@fzkqi) January 9, 2024
ついでにゲーミングモードも作ってみたw pic.twitter.com/JmDKzQinIA
リポジトリはこちら
アウトラインを描く仕組み
今回は最もシンプルな?法線方向へモデルを拡大して塗りつぶした画像を合成することでアウトラインを描く仕組みを実装しました
こちらのブログを大変参考にさせていただきました
他にもPost Effectを利用する方法もあるようです
アウトラインを付ける実装
こちらの記事で取り扱ったRealityKitのnonARモードで試してます
CustomMaterialの設定
アウトラインをつけたいEntityのクローンを作成し、CustomMaterialを設定し、子Entityとして追加します
surfaceShader
と geometryShader
はShaderで、実装は後述します
GeometryModifierでモデルを拡大し、SurfaceShaderで任意の色で塗りつぶします
カリングで前方を指定し、被っている範囲は描画しないようにします
let plane: Entity // アウトラインをつけたいEntity
let clone = plane.clone(recursive: true)
let device = MTLCreateSystemDefaultDevice()!
let library = device.makeDefaultLibrary()!
let surfaceShader = CustomMaterial.SurfaceShader(named: "surfaceShader", in: library)
let geometryShader = CustomMaterial.GeometryModifier(named: "geometryShader", in: library)
clone.modifyMaterials { original in
var mat = try! CustomMaterial(from: original, surfaceShader: surfaceShader, geometryModifier: geometryShader)
mat.faceCulling = .front
customMaterials.append(mat)
return mat
}
plane.addChild(clone)
// https://developer.apple.com/documentation/realitykit/altering_realitykit_rendering_with_shader_functions
extension Entity {
func modifyMaterials(_ closure: (RealityKit.Material) throws -> RealityKit.Material) rethrows {
try children.forEach { try $0.modifyMaterials(closure) }
guard var comp = components[ModelComponent.self] as? ModelComponent else { return }
comp.materials = try comp.materials.map { try closure($0) }
components[ModelComponent.self] = comp
}
}
Shaderの実装
まずはシンプルにアウトラインを描くShaderです
metalファイルに記述します
geometryShaderでモデルの各頂点を法線方向へ押し出し、
surfaceShaderでモデルを真っ黒に塗りつぶします
#include <metal_stdlib>
#include <RealityKit/RealityKit.h>
using namespace metal;
[[visible]]
void geometryShader(realitykit::geometry_parameters params) {
float3 norm = params.geometry().normal();
params.geometry().set_model_position_offset(norm * 0.4);
params.geometry().set_normal(-norm);
}
[[visible]]
void surfaceShader(realitykit::surface_parameters params) {
params.surface().set_base_color(half3());
params.surface().set_ambient_occlusion(0);
}
結果
シンプルな黒縁のアウトラインを描くことができました
Shaderに動的に値を渡す
UI上のスライダーでアウトラインの太さやゲーミングモードをコントロールするためには、Shaderに対して動的に値を渡す必要があります
動的に値を渡すためには、CustomMaterial.customを利用します
customに値を渡す
SIMD4を渡せるので、xにゲーミングの有効フラグ、yにゲーミングの色を流すタイミング、wにアウトラインの太さを入れます
var offset = 0
let width: Float = 0.2
// ---
offset %= 30
clone.setCustomVector(vector: .init(x: gaming ? 1 : 0, y: Float(offset) / 30, z: 0, w: width))
// https://developer.apple.com/documentation/realitykit/altering_realitykit_rendering_with_shader_functions
extension Entity {
func setCustomVector(vector: SIMD4<Float>) {
children.forEach { $0.setCustomVector(vector: vector) }
guard var comp = components[ModelComponent.self] as? ModelComponent else { return }
comp.materials = comp.materials.map { (material) -> Material in
if var customMaterial = material as? CustomMaterial {
customMaterial.custom.value = vector
return customMaterial
}
return material
}
components[ModelComponent.self] = comp
}
}
customの値を読み取る
Shaderで設定された値をparams.uniforms().custom_parameter()で読み取ります
geometryShaderではw、surfaceShaderではxとyを使用します
スクリーン上の位置とタイミングとyのタイミングを足して、色相に変換することで、流れるゲーミングを再現してます
#include <metal_stdlib>
#include <RealityKit/RealityKit.h>
using namespace metal;
[[visible]]
void geometryShader(realitykit::geometry_parameters params) {
float3 norm = params.geometry().normal();
params.geometry().set_model_position_offset(norm * params.uniforms().custom_parameter().w);
params.geometry().set_normal(-norm);
}
half3 hue2Rgba(half h) {
half hueDeg = h * 360.0;
half x = (1 - abs(fmod(hueDeg / 60.0, 2) - 1));
half3 rgba;
if (hueDeg < 60) rgba = half3(1, x, 0);
else if (hueDeg < 120) rgba = half3(x, 1, 0);
else if (hueDeg < 180) rgba = half3(0, 1, x);
else if (hueDeg < 240) rgba = half3(0, x, 1);
else if ( hueDeg < 300) rgba = half3(x, 0, 1);
else rgba = half3(1, 0, x);
return rgba;
}
[[visible]]
void surfaceShader(realitykit::surface_parameters params) {
auto cp = params.uniforms().custom_parameter();
float4 position = params.geometry().screen_position();
params.surface().set_base_color(cp.x > 0.5 ? hue2Rgba(position.x / 2000 + cp.y) : half3());
params.surface().set_ambient_occlusion(cp.x > 0.5 ? 1 : 0);
}
結果
アウトラインの太さを変更や、ゲーミングモードを実装できました
まとめ
RealityKitのCustomMaterialでアウトラインを描くShaderを作ってみました
情報が少なすぎたり、カメラを起動しないといけなかったりで、開発のハードルは高いですが、
RealityKitは思っていたより、様々な機能があり、柔軟性もありそうです
また、nonARモードとSimulatorを使用することで、ARのためのカメラを起動せずに開発できたので良かったです