안드로이드 AGSL 셰이더를 위한 Kotlin DSL: 타입 안전 접근법
RuntimeShader와 RenderEffect를 사용한 AGSL 셰이더 개발은 문자열 리터럴, 수동 uniform 바인딩, IDE 지원 부족 등의 문제로 골치 아픕니다. Kotlin DSL은 성능 손실 없이 네이티브 AGSL 코드를 생성하며 이러한 문제를 해결합니다. 이 라이브러리는 타입 안전한 구조를 즉시 사용 가능한 셰이더로 변환하고, agslSource()를 통해 원시 AGSL에 접근할 수 있게 합니다.
이렇게 하면 문자열 문법 대신 효과 로직에 집중할 수 있습니다. API 33+ 지원, Compose 통합, 프로시저럴 프리미티브 표준 라이브러리를 포함합니다.
원시 AGSL의 Kotlin에서의 고통스러운 점
문자열로 된 셰이더는 IDE의 자동완성, 리팩토링, 타입 체크 같은 편의 기능을 잃습니다. Uniform은 수동 선언—이름 불일치 시 앱이 크래시합니다. smoothstep, fract, mix 같은 복잡한 효과는 읽기 힘든 엉망진창이 됩니다. Compose에서는 시간과 업데이트를 위한 추가 보일러플레이트가 필요합니다.
DSL은 이런 장애물을 제거합니다: uniform은 타입화되고, 중간 계산은 let()으로 이름 지어지며, 표준 함수는 선언적으로 호출됩니다.
라이브러리 추가
build.gradle에 다음 의존성을 추가하세요:
dependencies {
implementation("io.github.i-redbyte:redbytefx-core:1.0.0")
implementation("io.github.i-redbyte:redbytefx-compose:1.0.0")
implementation("io.github.i-redbyte:redbytefx-stdlib:1.0.0")
}
비교: 웨이브 변위 효과
원시 AGSL
uniform shader content;
uniform float wave_amplitude;
uniform float wave_frequency;
half4 main(float2 fragCoord) {
float2 offset = float2(
0.0,
sin(fragCoord.x * wave_frequency) * wave_amplitude
);
return content.eval(fragCoord + offset);
}
Kotlin DSL
val effect = redbytefx {
val amplitudeUniform = uniformFloat(0f, "wave_amplitude")
val frequencyUniform = uniformFloat(0.08f, "wave_frequency")
val x = let(fragCoord.x, "x")
val waveOffset = let(
float2(0f, sin(x * frequencyUniform) * amplitudeUniform),
"wave_offset"
)
sample(fragCoord + waveOffset)
}
생성된 AGSL은 로직이 정확히 일치하지만, 소스 코드는 타입 안전하고 읽기 쉽습니다.
복잡도별 효과 예제
1. 웨이브: 기본 효과
sin을 사용한 간단한 Y-오프셋. fragCoord, sample(), uniform, let()을 배우기에 완벽합니다. 추가 복잡도 없이 단계 이름을 지정할 수 있습니다.
2. 시그널: 함수와 마스크
val effect = redbytefx {
val densityUniform = uniformFloat(8f, "signal_density")
val lineWidthUniform = uniformFloat(0.08f, "signal_line_width")
val amountUniform = uniformFloat(0.85f, "signal_amount")
val pulseBand = fn(
name = "pulse_band",
arg1 = FloatType,
arg2 = FloatType,
returns = FloatType
) { phase, threshold ->
step(threshold, smoothstep(0.08f, 0.92f, fract(phase)))
}
val base = let(sample(), "base")
val uv = let(normalizedUv(), "uv")
val grid = let(gridMask(uv, densityUniform, lineWidthUniform), "grid")
val scan = let(scanlines(fragCoord.y, 14f, 3f), "scan")
val pulse = let(pulseBand(uv.y * densityUniform * 0.5f + grid * 0.35f, 0.55f), "pulse")
val hardMask = let(step(0.45f, scan * pulse), "hard_mask")
val active = let((grid gt 0.05f) or (hardMask gt 0.5f), "active")
val accent = let(color(float3(0.05f, 0.95f, 0.82f), base.a), "accent")
val mixed = let(mix(base, accent, min(grid * 0.85f + hardMask * 0.35f, 1f)), "mixed")
ifElse(active, mix(base, mixed, amountUniform), base)
}
fn()으로 커스텀 함수 추출, stdlib의 gridMask()와 scanlines()로 마스크 간소화. ifElse()로 선언적 유지.
3. 레이더: 극좌표
val effect = redbytefx {
val time by autoUniformTime()
val speed by autoUniformFloat(0.72f)
val radius by autoUniformFloat(0.34f)
val amount by autoUniformFloat(0.86f)
val base = let(sample(), "base")
val uv = let(fragCoord / resolution, "uv")
val polar = let(polarCoordinates(uv), "polar")
val sweepAngle = let(fract(time * speed * 0.08f), "sweep_angle")
val sweep = let(angularSweep(uv = uv, angle = sweepAngle, width = 0.12f, feather = 0.03f), "sweep")
val arc = let(
arcMask(
uv = uv,
radius = radius,
ringWidth = 0.09f,
angle = sweepAngle,
arcWidth = 0.18f,
feather = 0.03f
),
"arc"
)
val outerRing = let(ringMask(uv, radius = radius, width = 0.016f, feather = 0.012f), "outer_ring")
val innerRing = let(ringMask(uv, radius = max(radius * 0.58f, 0.08f), width = 0.014f, feather = 0.012f), "inner_ring")
val beam = let(radialRamp(uv = uv, innerRadius = float(0.06f), outerRadius = radius + 0.18f), "beam")
val mask = let(max(max(sweep * beam, arc), max(outerRing, innerRing)), "mask")
val tint = let(
color(
mix(0.05f, 0.18f, polar.x * 1.4f),
mix(0.24f, 1f, sweep + arc * 0.55f),
mix(0.10f, 0.62f, polar.y * 0.45f + outerRing * 0.35f),
base.a
),
"tint"
)
val screened = let(maskedScreen(base, tint, mask, amount), "screened")
maskedOverlay(screened, color(float3(0.82f, 1f, 0.72f), base.a), arc, amount * 0.32f)
}
stdlib의 polarCoordinates(), arcMask(), ringMask()가 수동 smoothstep 체인을 대체. maskedScreen()과 maskedOverlay()로 블렌딩 간소화.
4. 메타볼: 프로시저럴 도형
네온 블롭을 위한 부드러운 min으로 SDF 원을 구축합니다. DSL은 복잡한 기하학을 읽기 쉽게 유지하며 전체 프로시저럴 효과를 처리합니다.
주요 요점
- Kotlin DSL은 오버헤드 없이 네이티브 AGSL 생성—
agslSource()로 확인. - 타입화된 uniform과
let()으로 문자열 코드와 리팩토링 문제를 해결. - 표준 라이브러리(gridMask, polarCoordinates, arcMask)가 프리미티브 작업 시간 절약.
fn()과ifElse()로 모듈화 지원, 명령형 피함.autoUniformTime()과 파라미터 바인딩으로 Compose 통합.
— Editorial Team
아직 댓글이 없습니다.