Bubble Effect

dk 158 Sep 20, 2005 at 14:10


This demo shows a procedurally generated pulsating soap bubble through dynamic deformation of a sphere that I have developed recently. The algorithm has been taken from ATI’s SDK, which provided the DirectX assembly shaders. I have written the HLSL/Cg shader code (below) myself after attempting to fully understand the theory behind the effect.

The vertex shader of the bubble effect uses sine waves to perturb the vertex position in the direction of the normal vector. The bubble uses two textures: a base texture and an environment texture. The base texture is a rainbow film map that simulates the rainbow effect on the bubble, which is then modulated with the environment map in the pixel shader. The glow map, which provides the white highlights on the bubble is stored in the alpha of the cube map, and is linearly interpolated into the result. The fresnel term and glow map are added to provide the final alpha value to blend with the frame buffer. The full explanation of the theory along with the full assembly shader code can be found at ATI’ developer’s page, which is an article that appeared in the ShaderX I book.

I’m providing the high-level code below, which should work in both HLSL and Cg.

Vertex shader:

struct VS_OUTPUT 
   float4 pos:      POSITION;
   float2 texCoord: TEXCOORD0;
   float4 reflection:   TEXCOORD1;
   float4 NdotV:    TEXCOORD2;

VS_OUTPUT main(float4 position: POSITION, float4 normal: NORMAL, float2 texCoord: TEXCOORD,
                float4 tangent: TANGENT,
                uniform float4x4 viewProjMatrix,
                uniform float4x4 worldMatrix,
                uniform float4 cameraPos,
                uniform float time)
    VS_OUTPUT Out;
    const float TWO_PI = 6.2831853072;
    const float4 wave_directions_in_X   = {0, 2, 0, 4};     // relative to u
    const float4 wave_directions_in_Y   = {2, 0, 4, 0};     // relative to v

    const float4 waveSpeed          = {0.6, 0.7, 1.2, 1.4};
    const float4 waveHeights        = {0.5, 0.5, 0.25, 0.25};

    // use texture coordinates as inputs to sinusoidal warp
    float4 wave_vec = frac( wave_directions_in_X*texCoord.x +
                wave_directions_in_Y*texCoord.y + waveSpeed*time );
    // shift the texture coordinates to be in (pi, -pi) range
    wave_vec = (wave_vec - 0.5) * TWO_PI;
    float4 wave_vec_sin = sin(wave_vec);
    float4 wave_vec_cos = (2 - cos(wave_vec))*0.04;     // multiply by 0.04 as fix up factor
    // dot with waveHeights and then apply deformation in the direction of the normal
    wave_vec = dot(wave_vec_sin,waveHeights) * normal + position;
    wave_vec.w = 1;     // homogeneous component
    // transform wave vector 
    Out.pos = mul(viewProjMatrix, wave_vec);
    // compute the binomial
    float4 binomial = float4(cross(tangent.xyz, normal.xyz), 1);

    // warp normal based on tangent and binomial vectors
    float4 warpedNormal = tangent  * dot(-(wave_vec_cos * waveHeights), wave_directions_in_Y) +
                    binomial * dot(-(wave_vec_cos * waveHeights), wave_directions_in_X);
    warpedNormal = warpedNormal + normal;

    // transform and normalize the normal
    warpedNormal = normalize(mul(worldMatrix, warpedNormal));
    // compute a normalized view vector
    float4 viewVector = normalize(cameraPos - mul(worldMatrix, wave_vec));
    // compute the reflection vector: R = 2*N(N.V) - V
    Out.reflection = 2*dot(warpedNormal.xyz, viewVector.xyz)*warpedNormal - viewVector;
    // pass to the pixel shader N.V
    Out.NdotV = dot(warpedNormal.xyz, viewVector.xyz);
    // pass along texture coordinates
    Out.texCoord = texCoord;

    return Out;

Pixel shader:

sampler baseMap: register(s0);
sampler cubeMap: register(s1);

float4 main(float2 texCoord: TEXCOORD0, float4 reflection: TEXCOORD1, float4 NdotV: TEXCOORD2 ) : COLOR
    float4 modulatedCubeMap, result;

    float4 base = tex2D(baseMap, texCoord);         // sample base texture
    float4 cube = texCUBE(cubeMap, reflection);     // sample cubemap
    modulatedCubeMap.rgb = saturate(2 * base * cube);   // modulate cube map with base map
    modulatedCubeMap.a = (1-abs(NdotV))*0.6 - 0.01;     // compute fresnel term by scaling alpha,
                                // multipling by N.V, and adding some bias
    // compute opacity from glow map
    float opacity = saturate(4*(cube.a*cube.a - 0.75));

    // linearly interpolate between (cubemap) and (base*cubemap) based on glow map
    result.rgb = lerp(modulatedCubeMap, cube, opacity);
    result.a = modulatedCubeMap.a + opacity;        // add fresnel term and glow map for alpha
    return result;

2 Replies

Please log in or register to post a reply.

anubis 101 Sep 20, 2005 at 14:34

Very nice… that’s how every image submission should be. Very well explained and in detail.

Mihail121 102 Sep 20, 2005 at 15:18