본문 바로가기
Shader CG

Liquid Glass

by SimonLee 2025. 6. 16.

타원을 만드는 코드

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