Войти
Totem 4 Engine BlogСтатьи

Totem Engine 4 Blog. Статья 6. Environment. Water Surface.

Автор:

article_8_water_1 | Totem Engine 4 Blog. Статья 6. Environment. Water Surface.

Это вторая статья про реализацию поверхности океана. На картинке выше представлено не максимальное качество, которого можно добиться при помощи описываемого метода.

Здесь представлена реализация воды с использованием карты высот размером 256х256 пикселей. Расчет такого размера карты позволяет добиться эффекта водной поверхности при значительной экономии ресурсов системы. Для финального качества рекомендуется использовать 512х512 или же еще больше при высоких мощностях системы.


1. Геометрия воды.

В текущей реализации для отображения воды я использую набор квадратный патчей геометрии, которые по дистанции до камеры подменяют друг друга, показывая большую или меньшую детализацию. Выглядит сетка так:

article_8_water_2 | Totem Engine 4 Blog. Статья 6. Environment. Water Surface.

Подобный алгоритм используется в примере nVidia SDK 10 Ocean. Фактически он реализует ручную тесселяцию геометрии (назову так). Алгоритм довольно сложен в реализации по той причине, что весьма запутан — необходимо между лодами геометрии на краях реализовывать промежуточный уровень патчей, который на внутренней стороне имеет двухкратную детализацию относительно внешней стороны. Для определения видимости патчей используется квадратичное дерево.

Могу порекомендовать при возможности использовать hardware tesselation. Возможно будет медленнее, но если не перебарщивать с качеством то не значительно.

Раньше я использовал для воды проекционную сетку, когда поверхность воды создается как одна плоскость, вершины которой получают позицию путем проекции координаты из камеры на плоскость. Плюс в том что мы видим всегда одинаковую детализацию воды (вблизи камеры большую, вдали меньшую) и получаем стабильный FPS, но реализация менее тривиальна чем мой текущий вариант, на мой взгляд, и кроме этого при большой дистанции от камеры до плоскости воды в случае ручной тесселяции мы можем получить значительный прирост fps связанный с уменьшением числа видимых вершин сетки воды. Это при условии, конечно, что сетка правильно настроена, потому что в обратном случае я получал результат противоположный.

Я думаю, что не имеет смысла описывать подробно принцип генерации патчей, каждый кто справился с Быстрым Преобразованием Фурье на данном этапе сумеет сам сгенерировать вершинные и индексные буферы.

Если кратко, чтобы навести на мысль, я делаю так — использую один буфер вершин для всех патчей и, забегая вперед, один индексный буфер со смещениями и размером внутри для каждого патча. Вершинный буфер содержит регулярную сетку максимальной детализации — скажем 16х16 вершин. По этой сетке для каждого патча создаются свои индексы. Для примера маленькая картинка

article_8_water_3 | Totem Engine 4 Blog. Статья 6. Environment. Water Surface.

На каждый лод 3 типа патча — центр, край и угол. Располагать их можно в любую сторону при помощи матрицы трансформации.

2. Формирование поверхности воды.

В прошлой статье я описал как получить основу воды — три основные карты: карту высот (height map), карту смещения в плоскости (choppy map) и карту нормалей (normal map). Для пустой воды, без объектов, этого достаточно, но для нормальной сцены нам понадобится больше.

Нам нужно подготовить сцену для отображения совместно с водой. Исключая тени (о которых я не буду писать в текущей статье, возможно позже) нам нужно 3 текстуры — отражения, преломления, и карта глубины.

Отражение объектов мы получаем путем рендера сцены в отраженную относительно плоскости воды камеру. Отразить камеру можно путем отражения матрицы вида относительно плоскости. Математику не подскажу — пользуюсь DX математической библиотекой.

Когда мы отображаем объекты в этом проходе нас интересуют только те, что находятся над поверхностью воды. Чтобы не иметь артефактов при рендере воды мы должны избавиться от подводной части объектов, находящихся на стыке сред. Можно отсекать плоскость, а можно заливать нижнюю часть объектов цветом глубокой воды. Этим же цветом должна быть залита сама текстура. Вот так выглядит стандартный шейдер рендера меша для отражений.

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
// matrixes
float4x4  g_MatrixViewProj;
float4x4  g_MatrixWorld;

// light
float3    g_LightDir;
float4    g_LightDiffuse;
float4    g_LightAmbient;

// material
float4    g_MaterialDiffuse;


// fog: x - max distance
float4    g_ReflectionWaterFog;
// textures
texture    g_TextureDiffuse    : register(s0);


//--------------------------------------------------------------------------------------
// Structures
//--------------------------------------------------------------------------------------
struct vertexInput 
{
    float3 position        : POSITION;
    float3 normal        : NORMAL;
    float2 texCoordDiffuse    : TEXCOORD0;
};

struct vertexOutput 
{
    float4 Position      : POSITION;
    float2 texCoordDiffuse  : TEXCOORD1;
    float3 WorldNormal     : TEXCOORD2;
  float  HeightMesh    : TEXCOORD3;
};

//--------------------------------------------------------------------------------------
// Samplers
//--------------------------------------------------------------------------------------
sampler TextureSampler = sampler_state 
{
    texture = <g_TextureDiffuse>;
};

//--------------------------------------------------------------------------------------
// Vertex shader
//--------------------------------------------------------------------------------------
vertexOutput VS_Reflection( vertexInput IN ) 
{
    vertexOutput OUT;
  
  // view space pos
    OUT.Position    = mul( float4(IN.position.xyz , 1.0) , mul( g_MatrixWorld, g_MatrixViewProj ) );
  // world normal 
  OUT.WorldNormal    = mul( IN.normal, (float3x3)g_MatrixWorld );
  // texture coord
    OUT.texCoordDiffuse = IN.texCoordDiffuse;  

  // mesh world pos
  float3 WorldPos    = mul( float4(IN.position.xyz , 1.0), g_MatrixWorld ); 
  // mesh height
  OUT.HeightMesh    = WorldPos.y;
  
    return OUT;
}

//--------------------------------------------------------------------------------------
// Pixel shader
//--------------------------------------------------------------------------------------
float4 PS_Reflection( vertexOutput IN ) : COLOR
{
  float4 Color = float4( 1.0f, 1.0f, 1.0f, 1.0f );
    
    // Compute lighting amount
  float4 Diffuse = dot( normalize( IN.WorldNormal ), normalize(-g_LightDir.xyz) );              
  Diffuse = min( saturate( Diffuse ) + g_LightAmbient, 1.0f );              
            
    // Lookup texture color
  float4 TexColor = tex2D( TextureSampler, IN.texCoordDiffuse );

    // Modulate texture color with lighting amount
  Color.rgb  = g_LightDiffuse.rgb * g_MaterialDiffuse.rgb * TexColor.rgb * Diffuse.rgb;
  Color.a    = g_MaterialDiffuse.a * TexColor.a;
  
  // per pixel fog for under water part
  float fFade = IN.HeightMesh >= g_ReflectionWaterFog.y ? 0.0f: saturate( -IN.HeightMesh / g_ReflectionWaterFog.x );

  // interpolation
  Color.a   = lerp( Color.a, 0.0f, fFade );
    

  return Color;
}

Преломления мы получаем путем рендера объектов обычной камерой, но только тех, которые находятся под водой. Для объектов на стыке сред мы можем применить отсечение, но лучше этого не делать, так как при преломлении мы будем использовать текстурные координаты большей области, чем сам меш. Все лишнее мы сумеем отсечь попиксельно в шейдере воды при помощи карты глубины.

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
// matrixes
float4x4  g_MatrixViewProj;
float4x4  g_MatrixWorld;

// light
float3    g_LightDir;
float4    g_LightDiffuse;
float4    g_LightAmbient;

// material
float4    g_MaterialDiffuse;
float4    g_MaterialSpecular;
float    g_MaterialPower;

// water scale : x - height scale, y - height offset, z - choppy y scale, w - water tile size
float4    g_WaterScale;
// deep water color
float4    g_WaterColor;
// fog: x - max distance, y - start under water clear
float4    g_UnderWaterFog;


// textures
texture    g_TextureDiffuse      : register(s0);

//texture    g_TextureDisplacement  : register(s257);

//--------------------------------------------------------------------------------------
// Structures
//--------------------------------------------------------------------------------------
struct vertexInput 
{
    float3 position        : POSITION;
    float3 normal        : NORMAL;
    float2 texCoordDiffuse    : TEXCOORD0;
};

struct vertexOutput 
{
    float4 Position      : POSITION;
    float2 texCoordDiffuse  : TEXCOORD1;
    float3 WorldNormal     : TEXCOORD2;
  float  HeightMesh    : TEXCOORD3;
};

//--------------------------------------------------------------------------------------
// Samplers
//--------------------------------------------------------------------------------------
sampler TextureSampler = sampler_state 
{
    texture = <g_TextureDiffuse>;
};


//--------------------------------------------------------------------------------------
// Vertex shader
//--------------------------------------------------------------------------------------
vertexOutput VS_Refraction( vertexInput IN ) 
{
    vertexOutput OUT;
  
  // view space pos
    OUT.Position    = mul( float4(IN.position.xyz , 1.0) , mul( g_MatrixWorld, g_MatrixViewProj ) );
  // world normal 
  OUT.WorldNormal    = mul( IN.normal, (float3x3)g_MatrixWorld );
  // texture coord
    OUT.texCoordDiffuse = IN.texCoordDiffuse;  
 
  // mesh world pos
  float3 WorldPos    = mul( float4(IN.position.xyz , 1.0), g_MatrixWorld ); 
  // fog
//  OUT.Fog        = WorldPos.y;
  // mesh height
  OUT.HeightMesh    = WorldPos.y;
  
    return OUT;
}

//--------------------------------------------------------------------------------------
// Pixel shader
//--------------------------------------------------------------------------------------
float4 PS_Refraction( vertexOutput IN ) : COLOR
{
  float4 Color = float4( 1.0f, 1.0f, 1.0f, 1.0f );
    
    // Compute lighting amount
  float4 Diffuse = dot( normalize( IN.WorldNormal ), normalize(-g_LightDir.xyz) );              
  Diffuse = min( saturate( Diffuse ) + g_LightAmbient, 1.0f );              
            
    // Lookup texture color
  float4 TexColor = tex2D( TextureSampler, IN.texCoordDiffuse );

    // Modulate texture color with lighting amount
  Color.rgb  = g_LightDiffuse.rgb * g_MaterialDiffuse.rgb * TexColor.rgb * Diffuse.rgb;
  Color.a    = g_MaterialDiffuse.a * TexColor.a;
  
  // per pixel fog with water height accuracy
  float fFade = IN.HeightMesh >= g_UnderWaterFog.y ? 
                    saturate( IN.HeightMesh / g_UnderWaterFog.z ) : 
                    saturate( -IN.HeightMesh / g_UnderWaterFog.x );

  // interpolation
  Color     = lerp( Color, g_WaterColor, fFade );
    
  return Color;
}

Ну как создать карту глубины я думаю все на этом этапе знают, единственное что стоит отметить, что можно объединить проход преломлений и глубины с использованием MRT, если в этой карте записана именно глубина преломленной сцены.

Ну теперь все готово для финального рендера. Для начала смотрим шейдер:

//--------------------------------------------------------------------------------------
// Global variables
//--------------------------------------------------------------------------------------
// matrixes
float4x4  g_MatrixViewProj;
float4x4  g_MatrixView;
float4x4  g_MatrixWorld;

// data
float4    g_CurrentTime;
// camera
float4    g_CameraPos;
// screen resolution : width, height
float4    g_ScreenSize;
// render texture resolution : width, height
float4    g_RenderTexSize;
// foam data : foam tex scale, foam start height, foam attenuation height
float4    g_FoamData;

// light
float3    g_LightDir;
//float3    g_LightPosition;
float4    g_LightDiffuse;
float4    g_LightAmbient;
float4    g_LightSpecular;
// x - power, y - intensity
float4    g_LightSpecularPower;

// water scale : x - height scale, y - height offset, z - choppy y scale, w - water tile size
float4    g_WaterScale;
// bend reflect ray from nVidia SDK Ocean (0.1f, -0.4f, 0.2f)
//float4    g_ReflectBendParam;
// refraction params : x - distortion force, y - underwater fog force
float4    g_ReflectRefractParam;

// materials
// deep water color
float4    g_WaterColor;
// x, y color intensity (for scattering from nVidia SDK Island)
float4    g_WaterColorIntensity;
// for scattering from nVidia SDK Island
float4    g_WaterScatterColor;
float4    g_SkyColor;


// textures
texture    g_TextureNormal     : register(s0);
texture   g_TextureFresnel     : register(s1);

texture    g_TextureReflection   : register(s2);
texture    g_TextureRefraction    : register(s3);

texture    g_TextureFoam       : register(s4);
texture    g_TextureHeightMap    : register(s5);
texture    g_TexturePerlin      : register(s6);

texture    g_TextureSceneDepth   : register(s7);

texture    g_TextureShadowSplit0  : register(s8);
texture    g_TextureShadowSplit1  : register(s9);
texture    g_TextureShadowSplit2  : register(s10);

texture    g_TextureDisplacement  : register(s257);

//--------------------------------------------------------------------------------------
// Structures
//--------------------------------------------------------------------------------------
struct vertexInput 
{
    float3 position      : POSITION;
};

struct vertexOutput 
{
    float4 Position      : POSITION;
  float4 WorldPos      : TEXCOORD0;
    float2 TexCoord     : TEXCOORD1;
  float2 Depth      : TEXCOORD2;
  float3 ToLightDir    : TEXCOORD3;
  float3 ToCameraDir    : TEXCOORD4;

};

//--------------------------------------------------------------------------------------
// Samplers
//--------------------------------------------------------------------------------------
sampler NormalSampler = sampler_state 
{
    texture = <g_TextureNormal>;
};

sampler FresnelSampler = sampler_state 
{
    texture = <g_TextureFresnel>;
};

sampler ReflectionSampler = sampler_state 
{
    texture = <g_TextureReflection>;
};

sampler RefractionSampler = sampler_state 
{
    texture = <g_TextureRefraction>;
};

sampler DisplacementSampler = sampler_state 
{
    texture = <g_TextureDisplacement>;
};

sampler FoamSampler = sampler_state 
{
    texture = <g_TextureFoam>;
};

sampler HeightSampler = sampler_state 
{
    texture = <g_TextureHeightMap>;
};

sampler PerlinSampler = sampler_state 
{
    texture = <g_TexturePerlin>;
};

sampler SceneDepthSampler = sampler_state 
{
    texture = <g_TextureSceneDepth>;
};


//--------------------------------------------------------------------------------------
// Vertex shader
//--------------------------------------------------------------------------------------
vertexOutput VS_WaterPlane( vertexInput IN ) 
{
    vertexOutput OUT;
  
  // world space init vertex position
  float3 f3BeginWorldPos  = mul( float4(IN.position.x, 0.0f, IN.position.y, 1.0f ), g_MatrixWorld );
  // copy
  float3 f3Position     = f3BeginWorldPos;
  
  // texture coord
  OUT.TexCoord  = float2( f3BeginWorldPos.x / g_WaterScale.w, f3BeginWorldPos.z / g_WaterScale.w );
  
  // map for fetch
  float4 HeightChoppyMap = tex2Dlod( DisplacementSampler, float4( OUT.TexCoord, 0.0f, 0.0f ) );
  
  // vertex displacement
  
  // distance Attenuation for artefacts removing
  
  // choppy
  f3Position.x  += HeightChoppyMap.y * g_WaterScale.z;
  f3Position.z  += HeightChoppyMap.z * g_WaterScale.z;
  // height
  f3Position.y  = HeightChoppyMap.x * g_WaterScale.x + g_WaterScale.y;
    
  // world final vertex position
  OUT.WorldPos    = float4( f3Position, 1.0f );

  // vector from light to vertex
  // really far from world origin
  OUT.ToLightDir    = normalize(-g_LightDir * 10000.0f - OUT.WorldPos);

  // view space vertex position
  OUT.Position    = mul( OUT.WorldPos, g_MatrixViewProj);
  
  // vector from camera to vertex
  OUT.ToCameraDir    = g_CameraPos.xyz - OUT.WorldPos;   

  // depth coordinate
    OUT.Depth.xy     = OUT.Position.zw;
  

    return OUT;
}

//--------------------------------------------------------------------------------------
// Pixel shader
//--------------------------------------------------------------------------------------
float4 PS_WaterPlane( vertexOutput IN, float2 inScreenPos : VPOS ) : COLOR
{
  float4 Color = float4( 1.0f, 1.0f, 1.0f, 1.0f );
  
  // vector from camera to pixel
  float3 f3PixelToCamera  = normalize(IN.ToCameraDir);

  // vector from light to pixel
  float3 f3PixelToLight  = normalize(IN.ToLightDir);

  // unpack normal
  float3 f3Normal    = 2.0f * tex2D( NormalSampler, IN.TexCoord )-1.0f;
    
  // screen uv
  float2 f2ScreenUV  = float2(  inScreenPos.x / g_ScreenSize.x + 0.5f / g_ScreenSize.x, 
                   inScreenPos.y / g_ScreenSize.y + 0.5f / g_ScreenSize.y );
                                    
  // water depth for clear refraction artefacts
  float fWaterDepth  = IN.Depth.x / IN.Depth.y;
  
  // SHADOW TERM
  
  float ShadowTerm = 0.0f;
  

  
  // FRESNEL COEFFICIENT
  
  // fresnel 
  float fCosAngle = dot(f3Normal,f3PixelToCamera);

  float4 f4Fresnel  = tex1D( FresnelSampler, fCosAngle ).bgra;  
  
  // REFLECTION TERM

  // reflected ray
  float3 f3ReflectedRay = reflect(-f3PixelToCamera, f3Normal);


  
  // reflection coordinats
  float2 f2ReflTexCoord = f2ScreenUV - f3Normal.xz * g_ReflectRefractParam.x;

  // reflection map
  float4 f4Reflection = tex2D( ReflectionSampler, f2ReflTexCoord );

  // empty space - sky color
  f4Reflection = f4Reflection.a < 0.001f ? g_SkyColor : f4Reflection;
  
  // Blend with predefined sky color
  f4Reflection.xyz = lerp(g_SkyColor, f4Reflection, f4Fresnel.y);
  
  // WATER COLOR
  
  // apply distance fog
  float4 f4WaterColor = g_WaterColor;

    // REFRACTION TERM
    
  // distortion
  float2 f2DistortTexCoord = f2ScreenUV + f3Normal.xz * g_ReflectRefractParam.y;

  // scene depth in refracted pos
  float fSceneDepth  = tex2D( SceneDepthSampler, f2DistortTexCoord ).r;
    
  // distortion : distortion under water only
  f2DistortTexCoord = fWaterDepth > fSceneDepth ? f2ScreenUV : f2DistortTexCoord;

  // refraction color
  float4 f4Refraction  = tex2D( RefractionSampler, f2DistortTexCoord );
  
  // refraction depth
  float fRefractionDepth = fSceneDepth - fWaterDepth;
  fRefractionDepth  = max(0, fRefractionDepth);
    
  // SUN SPECULAR TERM
    
  float fSpecularFactor = f4Fresnel.x * pow(max(0,dot(f3PixelToLight, f3ReflectedRay)), g_LightSpecularPower.x);
  
  // SCATTER TERM
  
  // from nVidia SDK Island 11
  // simulating scattering/double refraction: light hits the side of wave, travels some distance in water, and leaves wave on the other side
  // it's difficult to do it physically correct without photon mapping/ray tracing, so using simple but plausible emulation below
  
  // only the crests of water waves generate double refracted light
  float fScatterFactor = g_WaterColorIntensity.w*max(0, IN.WorldPos.y*0.25f + 0.25f);

  // the waves that lie between camera and light projection on water plane generate maximal amount of double refracted light 
  fScatterFactor  *= pow(max(0.0, dot(normalize(float3(f3PixelToLight.x, 0.0f, f3PixelToLight.z)),-f3PixelToCamera)),2.0);

  // the slopes of waves that are oriented back to light generate maximal amount of double refracted light 
  fScatterFactor  *= pow(max(0.0,1.0-dot(f3PixelToLight, f3Normal)),8.0);
  
  // water crests gather more light than lobes, so more light is scattered under the crests
  fScatterFactor  += 1.5*g_WaterColorIntensity.y * max(0, IN.WorldPos.y+1)*
    // the scattered light is best seen if observing direction is normal to slope surface
    max(0, dot(f3PixelToCamera, f3Normal))*
    // fading scattered light out at distance and if viewing direction is vertical to avoid unnatural look
    max(0, 1-f3PixelToCamera.y)*(g_WaterColorIntensity.z/(g_WaterColorIntensity.z + length(g_CameraPos.xyz - IN.WorldPos)));

  // fading scatter out by 90% near shores so it looks better
  fScatterFactor  *= 0.1 + 0.9 * fWaterDepth;
  
  // FOAM TERM
  
  // tex coord
  float2 f2FoamTexCoord = IN.TexCoord * g_FoamData.x;
  // disturbance
  f2FoamTexCoord += g_FoamData.x * 0.02f * f3Normal.xz;
  
  // texture
  float4 f4FoamColor = tex2D( FoamSampler, f2FoamTexCoord );
  // height in pix
  float fHeight = tex2D( HeightSampler, IN.TexCoord ).x * g_WaterScale.x + g_WaterScale.y;

  // attenuation
  f4FoamColor.a *= saturate((fHeight - g_FoamData.y) / g_FoamData.z );
  // distance attenuation
  f4FoamColor.a *= saturate( 1.0f - length(IN.ToCameraDir) / g_FoamData.w );
  
  // noise randomisation
  f4FoamColor.a *= saturate( tex2D( PerlinSampler, IN.TexCoord * 0.2f ).a );
  
  // WATER FINAL
  
  // base color
  float3 f3WaterBaseColor = lerp(f4Refraction.rgb, f4Reflection.rgb, f4Fresnel.x);

  Color.rgb  = f3WaterBaseColor;
  
  // foam
  Color.rgb  += f4FoamColor.a * f4FoamColor.rgb;
  
  // sun specular with foamy atten
  Color.rgb  += (1.0f - f4FoamColor.a ) * g_LightSpecularPower.y * fSpecularFactor * g_LightSpecular.rgb * f4Fresnel.x;

  // scatter
  Color.rgb  += g_WaterScatterColor * fScatterFactor;
  
  // shadow term
  Color.rgb  = lerp( f3WaterBaseColor, Color.rgb, ShadowTerm );

    

  return Color;
}

Вершинный шейдер довольно прост — основная идея заключается в том, что нам необходимо анимировать сетку воды по 3 направлениям. Для этого мы читаем данные из карты смещений, которая имеет строго 32 бита на канал для SM3.0. Получаемые значения устанавливаем вершинам.

Так же мы высчитываем дополнительные данные для пиксельного шейдера — позицию источника света и глубины воды в точке.

Пиксельный шейдер сложнее. Для начала нам нужна нормаль из карты нормалей, она определяет согласно формуле Френеля (посчитана заранее в текстуре, смотреть код в примере nVidia Ocean) степень видимости отражения либо преломления в точке воды. Цвет того и другого мы получаем из текстур, которые мы рендерили для сцены ранее, для этого нужно использовать координаты пикселя воды в экранном пространстве, перевести их в координаты текстуры с учетом их смещения по нормали. Таким образом мы получаем расплывчатое изображение.

Отдельно следует сказать, что для преломлений проводится тест по глубине, в котором отсеивается та часть изображения, которая не находится под водой.

В принципе, на этом этапе мы уже получим приемлемый внешний вид воды, но всегда хочется большего. Я использую некоторые визуальные улучшения, которые я подглядел в других примерах.

Первый эффект — это просвечивание гребней волн, что добавляет большую динамическую контрастность сцене. Эффект не требует каких-то дополнительных предварительных расчетов, поэтому может быть реализован в финальной стадии.

Второй эффект — наложение на высокие волны пены из заранее нарисованной текстуры пены. Мы накладываем волны не только по высоте волны, но и по текстуре шума, в которой от 0 до 1 значение указывает на наличие ли же отсутствие пены. Это позволяет избежать сильной заметности тайлов текстур поверхности воды.

3. Заключение

Самая важная часть статьи — шейдер поверхности воды. Ничего сложного нет, все довольно тривиально.

4. Источники

1. nVidia SDK OceanCS Sample (не смогу найти ссылку, битый сайт у вендора).

2. nVidia SDK Island11 Sample (то же самое, смотрите в самих пакетах SDK).

#ocean, #render, #Totem 4 Engine, #вода, #Игровые движки

17 октября 2012

Комментарии [39]

http://gdefile.ru/gde-ispolzuetsya-massovye-rassylki-soobshhenij.html , http://branding.allmedia.ru/PressReleasebranding/PressReleaseShow.asp?ID=432509