타원을 만드는 코드
a : x축 반지름, b : y축 반지름
n > 2 면 사각형에 가까워지고, n < 2 더 둥글게 퍼진 모양이 됨.
float sdSuperellipse(float2 p, float r, float n) {
p = abs(p / r);
float m = pow(p.x, n) + pow(p.y, n);
return (pow(m, 1.0 / n) - 1.0) * r;
}
리턴값의 의미
- 리턴값이 0이면 경계, 리턴값이 음수면 원안쪽, 양수면 원 바깥쪽.
float w1 = exp(-dg1 * dg1 * 8.0);
float w2 = exp(-dg2 * dg2 * 8.0);
float totalWeight = w1 + w2 + 1e-6;
dg1 원내부는 음수, 원 밖은 양수, 경계는 0이다.
-1을 한쪽에 곱해주면 -dg1 * dg1은 항상 음수가 나온다.
음수 지수승은 값을 1 이하로 만들어주기 때문에,
dg1 값이 0이면 가중치가 1이고, dg 값이 커질수록 w1는 작아진다.
이는 픽셀과 경계가 멀어질수록 영향력이 떨어지는 것으로 이해하면 된다.
package com.example.glasseffect
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
/**
* 메인 액티비티 - 유리 효과를 보여주는 앱의 진입점
* 배경 이미지 위에 투명한 유리 두 개가 상호작용하는 효과를 구현
*/
class MainActivity : AppCompatActivity() {
private lateinit var rootLayout: FrameLayout // 배경 이미지를 담는 루트 컨테이너
private lateinit var glassView: GlassView // 유리 효과를 렌더링하는 커스텀 뷰
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 루트 레이아웃 생성 및 배경 이미지 설정
rootLayout = FrameLayout(this).apply {
// R.drawable.wallpaper를 배경으로 설정 (유리를 통해 굴절되어 보일 이미지)
background = ContextCompat.getDrawable(this@MainActivity, R.drawable.wallpaper)
}
// 2. 글래스뷰 생성 - 전체 화면 크기로 설정
glassView = GlassView(this).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, // 전체 화면 너비
FrameLayout.LayoutParams.MATCH_PARENT // 전체 화면 높이
)
}
// 3. 뷰 계층 구조 설정
rootLayout.addView(glassView) // 배경 위에 유리뷰 올리기
setContentView(rootLayout) // 액티비티의 메인 뷰로 설정
// 4. 레이아웃 완료 후 셰이더 설정
// post {}를 사용하는 이유: 뷰의 크기가 측정되고 배치된 후에 셰이더를 초기화하기 위함
rootLayout.post {
glassView.setupGlassEffect(rootLayout)
}
// 5. 터치 이벤트 리스너 설정
setupTouchListener()
}
/**
* 터치 이벤트 처리 설정
* 사용자가 화면을 터치하면 오른쪽 유리가 터치 위치를 따라다니도록 함
*/
private fun setupTouchListener() {
var isTouching = false // 현재 터치 상태 추적
glassView.setOnTouchListener { _, event ->
when (event.action) {
// 터치 시작
MotionEvent.ACTION_DOWN -> {
isTouching = true
glassView.updateMousePosition(event.x, event.y, true)
android.util.Log.d("Touch", "DOWN: ${event.x}, ${event.y}")
true
}
// 터치하며 이동
MotionEvent.ACTION_MOVE -> {
if (isTouching) {
// 실시간으로 유리 위치 업데이트
glassView.updateMousePosition(event.x, event.y, true)
android.util.Log.d("Touch", "MOVE: ${event.x}, ${event.y}")
}
true
}
// 터치 종료
MotionEvent.ACTION_UP -> {
isTouching = false
glassView.updateMousePosition(event.x, event.y, false)
android.util.Log.d("Touch", "UP: ${event.x}, ${event.y}")
true
}
else -> false
}
}
}
/**
* 커스텀 글래스뷰 클래스
* Android의 RuntimeShader를 사용해 GLSL 셰이더로 유리 효과를 구현
*/
class GlassView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
// 셰이더 관련 변수들
private var currentShader: RuntimeShader? = null // 현재 사용 중인 셰이더 객체
private var mouseX = 0f // 마우스/터치 X 좌표
private var mouseY = 0f // 마우스/터치 Y 좌표
private var isTouching = false // 터치 상태
/**
* 유리 효과 셰이더 설정
* 부모 뷰의 배경을 캡처하고 GLSL 셰이더를 초기화함
*/
fun setupGlassEffect(parentView: View) {
try {
// 1. 배경 이미지 캡처 - 셰이더에서 굴절 효과를 위해 사용
updateBackground(parentView)
// 2. GLSL 셰이더 코드 정의
val glassShader = """
// 셰이더 입력 변수들 (uniform)
uniform shader background; // 배경 이미지 텍스처
uniform float2 resolution; // 화면 해상도 (width, height)
uniform float2 mousePos; // 마우스/터치 위치
uniform float isTouching; // 터치 여부 (0.0 또는 1.0)
// === SDF (Signed Distance Function) 함수들 ===
/**
* IQ의 슈퍼타원 SDF (단순화 버전)
* 둥근 모서리를 가진 사각형 모양을 만드는 함수
* @param p: 현재 픽셀 위치
* @param r: 반지름
* @param n: 모서리 둥글기 (4.0이면 거의 원형)
*/
float sdSuperellipse(float2 p, float r, float n) {
p = abs(p / r); // 절댓값으로 변환하여 대칭성 확보
float m = pow(p.x, n) + pow(p.y, n); // 거듭제곱 합
return (pow(m, 1.0 / n) - 1.0) * r; // SDF 값 반환 (음수면 내부, 양수면 외부)
}
/**
* 체커보드 패턴 생성 (폴백용)
* 배경 이미지가 없을 때 사용할 격자 무늬
*/
float checker(float2 uv, float scale) {
float2 c = floor(uv * scale); // 격자 좌표 계산
return mod(c.x + c.y, 2.0); // 홀짝에 따라 0 또는 1 반환
}
/**
* 배경 샘플링 함수
* 주어진 UV 좌표에서 배경 이미지의 색상을 가져옴
*/
half3 sampleBackground(float2 uv) {
// UV 좌표를 실제 화면 픽셀 좌표로 변환
float2 screenCoord = uv * resolution.y + 0.5 * resolution;
// 배경 텍스처에서 색상 샘플링
half4 bgSample = background.eval(screenCoord);
return bgSample.rgb; // RGB 값만 반환
}
/**
* 부드러운 최솟값 함수 (Smooth Minimum)
* 두 SDF를 부드럽게 블렌딩하여 리퀴드 효과 생성
* @param a, b: 블렌딩할 두 SDF 값
* @param k: 블렌딩 강도 (클수록 더 부드러움)
*/
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
/**
* 효율적인 블러 근사 함수
* 9개 픽셀 샘플링으로 가우시안 블러 효과 구현
*/
half3 efficientBlur(float2 uv, float blurStrength) {
half3 result = half3(0.0);
float totalWeight = 0.0;
// 3x3 커널로 주변 픽셀들 샘플링
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
float2 offset = float2(x, y) * blurStrength * 0.002;
float weight = 1.0 / (1.0 + length(float2(x, y))); // 거리 기반 가중치
result += sampleBackground(uv + offset) * weight;
totalWeight += weight;
}
}
return result / totalWeight; // 가중 평균 반환
}
// === 메인 셰이더 함수 ===
half4 main(float2 coord) {
// 1. 좌표계 정규화 (-0.5 ~ 0.5 범위로 변환)
float2 uv = (coord - 0.5 * resolution) / resolution.y;
// 2. 마우스 위치 정규화 및 종횡비 보정
float2 normalizedMouse = (mousePos / resolution) - 0.5;
normalizedMouse.x *= (resolution.x / resolution.y); // 종횡비 보정
float2 mouse = normalizedMouse;
// 3. 두 유리의 위치 정의
float2 pos1 = float2(-0.1, 0.0); // 왼쪽 고정 유리
// 터치 중이면 마우스 위치, 아니면 오른쪽 고정 위치
float2 pos2 = isTouching > 0.5 ? mouse : float2(0.1, 0.0);
// 4. 유리 크기 파라미터
float radius = 0.05; // 유리 반지름 (작은 크기)
float n = 4.0; // 모서리 둥글기 (4.0 = 거의 원형)
// 5. 각 유리에 대한 SDF 계산
float d1 = sdSuperellipse(uv - pos1, radius, n); // 왼쪽 유리
float d2 = sdSuperellipse(uv - pos2, radius, n); // 오른쪽 유리
// 6. 두 유리를 부드럽게 블렌딩 (리퀴드 효과)
float blendRadius = 0.05; // 블렌딩 강도
float d = smin(d1, d2, blendRadius); // 합쳐진 SDF
// 7. 그림자 효과 계산
float2 shadowOffset = float2(0.0, -0.01); // 아래쪽으로 그림자 오프셋
float shadowBlur = 0.05; // 그림자 블러 강도
// 그림자용 SDF 계산 (약간 아래쪽으로 이동된 위치)
float shadow1 = sdSuperellipse(uv - pos1 - shadowOffset, radius, n);
float shadow2 = sdSuperellipse(uv - pos2 - shadowOffset, radius, n);
float shadowSDF = smin(shadow1, shadow2, blendRadius);
// 그림자 마스크 생성 (부드러운 페이드아웃)
float shadowMask = 1.0 - smoothstep(0.0, shadowBlur, shadowSDF);
shadowMask *= 0.1; // 그림자 투명도 (10%)
// 8. 기본 배경 색상 및 그림자 적용
half3 baseColor = sampleBackground(uv);
baseColor = mix(baseColor, half3(0.0), shadowMask); // 그림자 어둡게
// 9. 유리 내부/외부 처리 분기
if (d < 0.0) {
// === 유리 내부 - 굴절 효과 적용 ===
// 9-1. 두 유리 중심의 가중 평균 계산
float w1 = exp(-d1 * d1 * 8.0); // 첫 번째 유리 가중치 (거리 기반)
float w2 = exp(-d2 * d2 * 8.0); // 두 번째 유리 가중치
float totalWeight = w1 + w2 + 1e-6;
// 블렌딩된 중심점 계산 (두 유리가 합쳐질 때 자연스러운 중심)
float2 center = (pos1 * w1 + pos2 * w2) / totalWeight;
// 9-2. 굴절 왜곡 계산
float2 offset = uv - center; // 중심으로부터의 거리벡터
float distFromCenter = length(offset); // 중심으로부터의 거리
// 가장자리로부터의 깊이 계산 (유리 두께 시뮬레이션)
float depthInShape = abs(d);
float normalizedDepth = clamp(depthInShape / (radius * 0.8), 0.0, 1.0);
// 지수적 왜곡 (가장자리에서 더 강한 굴절)
float edgeFactor = 1.0 - normalizedDepth;
float exponentialDistortion = exp(edgeFactor * 3.0) - 1.0;
// 9-3. 렌즈 굴절 강도 계산
float baseMagnification = 0.75; // 기본 확대율
float lensStrength = 0.4; // 렌즈 굴절 강도
float distortionAmount = exponentialDistortion * lensStrength;
// 9-4. 색수차 효과 (프리즘 효과)
float baseDistortion = baseMagnification + distortionAmount * distFromCenter;
// RGB 각 채널을 약간씩 다르게 굴절시켜 무지개 효과 생성
float redDistortion = baseDistortion * 0.9; // 빨간색 90%
float greenDistortion = baseDistortion * 1.0; // 초록색 100%
float blueDistortion = baseDistortion * 1.1; // 파란색 110%
// 각 색상 채널별 굴절된 좌표 계산
float2 redUV = center + offset * redDistortion;
float2 greenUV = center + offset * greenDistortion;
float2 blueUV = center + offset * blueDistortion;
// 9-5. 블러 및 색수차 적용
float blurStrength = edgeFactor * 0.0 + 1.5; // 블러 강도
// 각 색상 채널별로 블러링된 배경 샘플링
half3 redBlur = efficientBlur(redUV, blurStrength);
half3 greenBlur = efficientBlur(greenUV, blurStrength);
half3 blueBlur = efficientBlur(blueUV, blurStrength);
// RGB 채널 조합으로 최종 굴절 색상 생성
half3 refractedColor = half3(redBlur.r, greenBlur.g, blueBlur.b);
// 9-6. 유리 틴트 및 밝기 조정
refractedColor *= half3(0.95, 0.98, 1.0); // 약간 파란 틴트
refractedColor += half3(0.2); // 밝기 증가
// 9-7. 프레넬 반사 계산 (시점에 따른 반사)
float2 eps = float2(0.01, 0.0); // 미분 계산용 작은 값
// 법선 벡터 계산 (그래디언트 방법)
float2 gradient = float2(
// X방향 그래디언트
smin(sdSuperellipse(uv + eps.xy - pos1, radius, n),
sdSuperellipse(uv + eps.xy - pos2, radius, n), blendRadius) -
smin(sdSuperellipse(uv - eps.xy - pos1, radius, n),
sdSuperellipse(uv - eps.xy - pos2, radius, n), blendRadius),
// Y방향 그래디언트
smin(sdSuperellipse(uv + eps.yx - pos1, radius, n),
sdSuperellipse(uv + eps.yx - pos2, radius, n), blendRadius) -
smin(sdSuperellipse(uv - eps.yx - pos1, radius, n),
sdSuperellipse(uv - eps.yx - pos2, radius, n), blendRadius)
);
float3 normal = normalize(float3(gradient, 1.0)); // 3D 법선 벡터
float3 viewDir = float3(0.0, 0.0, -1.0); // 시점 방향 (화면 방향)
// 간단한 프레넬 계산 (시점과 법선의 각도에 따른 반사)
float fresnel = pow(1.0 - abs(dot(normal, viewDir)), 2.0);
half3 fresnelColor = half3(1.0); // 흰색 반사
// 굴절색과 반사색 블렌딩
half3 finalColor = mix(refractedColor, fresnelColor, fresnel * 0.3);
return half4(finalColor, 1.0); // 불투명한 유리
} else {
// === 유리 외부 - 가장자리 프레넬 효과 ===
float edgeDistance = abs(d); // 유리 가장자리로부터의 거리
float edgeThickness = 0.004; // 매우 얇은 가장자리 효과
if (edgeDistance < edgeThickness) {
// 가장자리 프레넬 효과 계산
float edgeFactor = 1.0 - (edgeDistance / edgeThickness);
edgeFactor = smoothstep(0.0, 1.0, edgeFactor); // 부드러운 페이드
// 대각선 하이라이트 패턴 (유리의 광택 효과)
float2 normalizedPos = uv * 1.5;
float diagonal1 = abs(normalizedPos.x + normalizedPos.y); // 주 대각선
float diagonal2 = abs(normalizedPos.x - normalizedPos.y); // 부 대각선
// 대각선 하이라이트 강도 계산
float diagonalFactor = max(
smoothstep(1.0, 0.1, diagonal1), // 주 대각선 하이라이트
smoothstep(1.0, 0.5, diagonal2) // 부 대각선 하이라이트
);
diagonalFactor = pow(diagonalFactor, 1.8); // 하이라이트 강도 조정
// 밝은 흰색 하이라이트와 배경색 블렌딩
half3 edgeWhite = half3(1.2); // 과포화된 흰색
half3 edgeColor = mix(baseColor, edgeWhite, diagonalFactor * edgeFactor);
// 최종 가장자리 반사 강도
float reflectionStrength = edgeFactor * 0.6;
// 배경과 가장자리 효과 블렌딩
return half4(mix(baseColor, edgeColor, reflectionStrength), reflectionStrength);
} else {
// 유리와 가장자리 밖 - 완전 투명
return half4(0.0, 0.0, 0.0, 0.0);
}
}
}
""".trimIndent()
// 3. RuntimeShader 객체 생성 및 초기화
currentShader = RuntimeShader(glassShader)
// 셰이더 uniform 변수들 초기값 설정
currentShader!!.setFloatUniform("resolution", width.toFloat(), height.toFloat())
currentShader!!.setFloatUniform("mousePos", width * 0.6f, height * 0.5f) // 초기 마우스 위치
currentShader!!.setFloatUniform("isTouching", 0f) // 초기에는 터치 안 함
// 4. RenderEffect 생성 및 적용
val renderEffect = RenderEffect.createRuntimeShaderEffect(currentShader!!, "background")
setRenderEffect(renderEffect) // 뷰에 셰이더 효과 적용
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 부모 뷰의 배경을 캡처하여 셰이더에서 사용할 비트맵 생성
* 유리 굴절 효과를 위해 배경 이미지가 필요함
*/
fun updateBackground(parentView: View) {
try {
val parentBackground = parentView.background
if (parentBackground != null && width > 0 && height > 0) {
// 1. 현재 뷰 크기와 같은 비트맵 생성
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
// 2. 부모 뷰의 배경을 비트맵에 그리기
canvas.save()
parentBackground.setBounds(0, 0, parentView.width, parentView.height)
parentBackground.draw(canvas) // 배경 drawable을 캔버스에 그리기
canvas.restore()
// 3. 생성된 비트맵을 뷰의 배경으로 설정 (셰이더에서 참조됨)
background = BitmapDrawable(resources, bitmap)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 마우스/터치 위치 업데이트 및 셰이더 uniform 변수 갱신
* 터치 이벤트가 발생할 때마다 호출되어 실시간으로 유리 위치 변경
*/
fun updateMousePosition(x: Float, y: Float, touching: Boolean) {
// 1. 내부 상태 업데이트
isTouching = touching
mouseX = x
mouseY = y
android.util.Log.d("GlassView", "updateMousePosition: x=$x, y=$y, touching=$touching")
// 2. 셰이더 uniform 변수들 업데이트
currentShader?.let { shader ->
try {
val touchingValue = if (touching) 1f else 0f
android.util.Log.d("GlassView", "Setting isTouching to: $touchingValue")
// 셰이더의 uniform 변수들 업데이트
shader.setFloatUniform("mousePos", mouseX, mouseY) // 마우스 위치
shader.setFloatUniform("isTouching", touchingValue) // 터치 상태
// 3. RenderEffect 재설정 (중요: 이것이 없으면 셰이더가 업데이트되지 않음)
val renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "background")
setRenderEffect(renderEffect)
android.util.Log.d("GlassView", "RenderEffect re-applied")
} catch (e: Exception) {
android.util.Log.e("GlassView", "Error updating shader uniforms", e)
e.printStackTrace()
}
} ?: run {
android.util.Log.w("GlassView", "currentShader is null!")
}
}
/**
* 뷰 크기 변경 시 셰이더 해상도 업데이트
* 화면 회전이나 크기 변경 시 자동 호출됨
*/
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 셰이더의 resolution uniform 변수 업데이트
currentShader?.setFloatUniform("resolution", w.toFloat(), h.toFloat())
}
}
}
728x90
'Shader CG' 카테고리의 다른 글
FireWorks (0) | 2024.09.29 |
---|---|
Star (0) | 2024.09.10 |
12. RayMarching - rain and lights (0) | 2024.08.18 |
11. RayMarching - Snow man (2) | 2024.08.14 |
10. RayMarching - Cube map (0) | 2024.08.13 |