Приводятся шейдеры вершин и фрагментов, позволяющие наложить текстуру, при наблюдении которой возникает иллюзия выступов и углублений (parallax mapping). Текстура наблюдается на квадрате (рис. 1).
Рис. 1. Parallax mapping
Для получения результата использованы C#, OpenGL библиотеки OpenTK.
Приводимый пример – это слегка модифицированный код Swizzled DXT5 Parallax Mapping, имеющийся в демонстрационном пакете OpenTK.
DDS-файлы образов, используемых для создания текстур, заимствованы с http://www.TyphoonLabs.com (DDS – Direct Draw surface file). Одновременно используются две текстуры: TMU0_Unit и TMU1_Unit.
GLSL-файлы шейдеров вершин и фрагментов входят в состав OpenTK (GLSL – Graphics Library Shader Language).
Используется консоль-проект, в пространство имен которого добавляются недостающие ссылки (Solution Explorre – References – правая кнопка мыши – Add Reference – Browse):
using System;
using OpenTK; // WindowState , Exit() and so on
using OpenTK.Graphics.OpenGL;
using OpenTK.Input; // KeyboardKeyEventArgs
Замечание. Техника создания иллюзии углублений и выступов рассматривается сайте Learn OpenGL. Advanced Lighting.
Описываемые далее файлы образов и шейдеров могут быть получены по следующей ссылке: скачать dds- и glsl-файлы
Файл swizzledRockDiffuseHeight.dds содержит карту диффузионного света и карту высоты.
Образ, хранимый файлом swizzledRockDiffuseHeight.dds имеет 8 mip-карт.
На рис. 2 показаны mip-карты первого и восьмого уровней.
Рис. 2. Образ для текстуры TMU0_Unit (mip-уровни 1 и 8)
На рис. 3 показаны свойства файла swizzledRockDiffuseHeight.dds для первого mip-уровня.
Рис. 3. Свойства swizzledRockDiffuseHeight.dds (выбран mip-уровень 1)
Файл swizzledRockNormalGloss.dds содержит карты нормалей и зеркально отражаемого света.
Файл swizzledRockNormalGloss.dds хранит 9 mip-карт, первая и девятая показаны на рис. 4.
Рис. 4. Образ для текстуры TMU1_Unit (mip-уровни 1 и 9)
На рис. 5 показаны свойства файла swizzledRockNormalGloss.dds для девятого mip-уровня.
Рис. 5. Свойства swizzledRockNormalGloss.dds (выбран mip-уровень 9)
Код шейдеров вершин и фрагментов загружается соответственно из файлов Parallax_VS.glsl и Parallax_FS.glsl.
Код шейдера вершин:
// Copyright (c) 2008 the OpenTK Team
// Custom vertex attribute
attribute vec3 AttributeTangent;
// world uniforms
uniform vec3 Light_Position;
uniform vec3 Camera_Position;
// MUST be written to for FS (передаются шейдеру фрагментов)
varying vec3 VaryingLightVector;
varying vec3 VaryingEyeVector;
void main()
{
gl_Position = ftransform();
gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
vec3 nor = normalize(gl_NormalMatrix * gl_Normal);
vec3 tan = normalize(gl_NormalMatrix * AttributeTangent);
vec3 bi = cross(nor, tan); // Векторное произведение векторов nor и tan
// Need positions in tangent space
vec3 vertex = vec3(gl_ModelViewMatrix * gl_Vertex);
vec3 temp = Light_Position - vertex;
VaryingLightVector.x = dot(temp, tan); // Скалярное произведение векторов temp и tan
VaryingLightVector.y = dot(temp, bi);
VaryingLightVector.z = dot(temp, nor);
temp = Camera_Position - vertex;
VaryingEyeVector.x = dot(temp, tan);
VaryingEyeVector.y = dot(temp, bi);
VaryingEyeVector.z = dot(temp, nor);
}
Код шейдера фрагментов:
// Copyright (c) 2008 the OpenTK Team
// Material uniforms
uniform sampler2D Material_DiffuseAndHeight;
uniform sampler2D Material_NormalAndGloss;
// Управляется клавишами Q, A, W, S, E и D
uniform vec3 Material_ScaleBiasShininess; // x = Scale, y = Bias, z = Shininess
// Light uniforms
uniform vec3 Light_DiffuseColor;
uniform vec3 Light_SpecularColor;
// From VS (получаем от шейдера вершин)
varying vec3 VaryingLightVector;
varying vec3 VaryingEyeVector;
vec3 normal;
void main()
{
vec3 lightVector = normalize(VaryingLightVector);
vec3 eyeVector = normalize(VaryingEyeVector);
// First, find the parallax displacement by reading only the height map (получаем смещение, используя карту высоты)
float parallaxOffset = texture2D(Material_DiffuseAndHeight, gl_TexCoord[0].st ).a *
Material_ScaleBiasShininess.x - Material_ScaleBiasShininess.y;
// Displace texcoords according to viewer (смещаем координаты текстуры)
vec2 newTexCoords = gl_TexCoord[0].st + (parallaxOffset * eyeVector.xy);
// Knowing the displacement, read RGB, Normal and Gloss
vec3 diffuseColor = texture2D(Material_DiffuseAndHeight, newTexCoords.st).rgb;
vec4 temp = texture2D(Material_NormalAndGloss, newTexCoords.st);
// Build a usable normal vector
normal.xy = temp.ag * 2.0 - 1.0; // Swizzle alpha and green to x/y and scale to [-1..+1]
normal.z = sqrt(1.0 - normal.x*normal.x - normal.y*normal.y); // z = sqrt(1-x^2-y^2)
// Move other properties to be better readable
float gloss = temp.r;
float lambert = max(dot(lightVector, normal), 0.0);
gl_FragColor = vec4(Light_DiffuseColor * diffuseColor, 1.0) * lambert;
if (lambert > 0.0)
{
// clamp – это min(max(x, minVal), maxVal)
float specular = pow(clamp(dot(reflect(-lightVector, normal), eyeVector), 0.0, 1.0), Material_ScaleBiasShininess.z);
gl_FragColor += vec4(Light_SpecularColor * diffuseColor, 1.0) * (specular * gloss);
}
}
Шейдер фрагментов получает от шейдера вершин векторы освещенности и наблюдения и далее, оперируя данными dds-файлов, вычисляет цвет текущего пикселя (gl_FragColor).
В приводимой ниже программе изображение управляется в результате изменения следующих параметров:
Первый параметр изменяется в результате нажатий на клавиши Q и A, второй - W и S, на третий влияют клавиши E и D.
На рис. 6 показан квадрат параметрами масштаб и смещение, равными 0.7, и с яркостью 1.0.
Рис. 6. Material_ScaleBiasShininess.X = 0.7; Material_ScaleBiasShininess.Y = 0.7, Material_ScaleBiasShininess.Z = 1
Кроме того, на результат влияют положение источника света и камеры, которые изменяются при перемещении мыши (см. OnMouseMove).
В приводимой далее программе создаются две текстуры:
Код приложения:
using System;
using System.IO;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using TextureLoaders;
namespace ParallaxMapping
{
// Демонстрация Swizzled DXT5 Parallax Mapping
public class T12_GLSL_Parallax : GameWindow
{
public T12_GLSL_Parallax() : base(800, 600) { }
// Шейдеры вершин и фрагментов
int VertexShaderObject, FragmentShaderObject, ProgramObject;
const string VertexShaderFilename = "G:/AM/Parallax_VS.glsl";
const string FragmentShaderFilename = "G:/AM/Parallax_FS.glsl";
const int AttribTangent = 5; // Slot where to pass tangents to VS, not sure which are reserved besides 0
Vector3 Tangent = new Vector3(1f, 0f, 0f);
Vector3 Normal = new Vector3(0f, 0f, 1f);
// Материал
//Vector3 MaterialScaleAndBiasAndShininess = new Vector3( 0.07f, 0.0f, 38.0f ); // Металл
Vector3 MaterialScaleAndBiasAndShininess = new Vector3(0.04f, 0.0f, 92.0f); // Камень
// Текстуры
const TextureUnit TMU0_Unit = TextureUnit.Texture0;
const int TMU0_UnitInteger = 0;
const string TMU0_Filename = "G:/AM/swizzledRockDiffuseHeight.dds";
uint TMU0_Handle;
TextureTarget TMU0_Target;
const TextureUnit TMU1_Unit = TextureUnit.Texture1;
const int TMU1_UnitInteger = 1;
const string TMU1_Filename = "G:/AM/swizzledRockNormalGloss.dds";
uint TMU1_Handle;
TextureTarget TMU1_Target;
// Камера
Vector3 EyePos = new Vector3(0.0f, 0.0f, 3.0f);
// Источник света
Vector3 LightPosition = new Vector3(0.0f, 1.0f, 1.0f);
Vector3 LightDiffuse = new Vector3(0.5f, 0.5f, 0.5f); // Серый
Vector3 LightSpecular = new Vector3(1f, 1f, 1f); // Белый
protected override void OnLoad(EventArgs e)
{
this.VSync = VSyncMode.Off;
// Проверки возможностей текущей версии OpenGL
string extensions = GL.GetString(StringName.Extensions);
if (!GL.GetString(StringName.Extensions).Contains("GL_ARB_shading_language"))
throw new NotSupportedException(String.Format("Пример тербует OpenGL версии 2.0. Найдена {0}. Отказ.",
GL.GetString(StringName.Version).Substring(0, 3)));
if (!extensions.Contains("GL_ARB_texture_compression") || !extensions.Contains("GL_EXT_texture_compression_s3tc"))
throw new NotSupportedException("Пример требует поддержки сжатия текстур. Отказ.");
int[] temp = new int[1];
GL.GetInteger(GetPName.MaxTextureImageUnits, out temp[0]);
Console.WriteLine(temp[0] + " TMU шейдеров фрагментов");
GL.GetInteger(GetPName.MaxVaryingFloats, out temp[0]);
Console.WriteLine(temp[0] + " вещественных параметров шейдера вершин доступно в шейдере фрагментов");
GL.GetInteger(GetPName.MaxVertexUniformComponents, out temp[0]);
Console.WriteLine(temp[0] + " однородных компонентов достуно в шейдере вершин");
GL.GetInteger(GetPName.MaxFragmentUniformComponents, out temp[0]);
Console.WriteLine(temp[0] + " однородных компонентов достуно в шейдере фрагментов");
Console.WriteLine("");
GL.ClearColor(0.2f, 0f, 0.4f, 0f); // Цвет фона
GL.PointSize(10f); // Размер точки, отображающей позицию источника света
GL.Disable(EnableCap.Dither);
GL.FrontFace(FrontFaceDirection.Ccw); // Лицевые грани обходятся против часовой стрелки
GL.PolygonMode(MaterialFace.Front, PolygonMode.Fill); // Способы вывода лицевых и нелицевых граней
GL.PolygonMode(MaterialFace.Back, PolygonMode.Line);
string LogInfo;
// Загрузка и компиляция шейдера вершин
using (StreamReader sr = new StreamReader(VertexShaderFilename))
{
VertexShaderObject = GL.CreateShader(ShaderType.VertexShader);
GL.ShaderSource(VertexShaderObject, sr.ReadToEnd());
GL.CompileShader(VertexShaderObject);
}
GL.GetShaderInfoLog(VertexShaderObject, out LogInfo);
if (LogInfo.Length > 0 && !LogInfo.Contains("hardware"))
Console.WriteLine("Ошибка компиляции шейдера вершин\nLog:\n" + LogInfo);
else
Console.WriteLine("Компиляция шейдера вершин завершена успешно");
// Загрузка и компиляция шейдера фрагментов
using (StreamReader sr = new StreamReader(FragmentShaderFilename))
{
FragmentShaderObject = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(FragmentShaderObject, sr.ReadToEnd());
GL.CompileShader(FragmentShaderObject);
}
GL.GetShaderInfoLog(FragmentShaderObject, out LogInfo);
if (LogInfo.Length > 0 && !LogInfo.Contains("hardware"))
Console.WriteLine("Ошибка компиляции шейдера фрагментов\nLog:\n" + LogInfo);
else
Console.WriteLine("Компиляция шейдера фрагментов завершена успешно");
// Связываем шейдеры с рабочей программой
ProgramObject = GL.CreateProgram();
GL.AttachShader(ProgramObject, VertexShaderObject);
GL.AttachShader(ProgramObject, FragmentShaderObject);
// Прежде следует определить атрибут "AttributeTangent"
GL.BindAttribLocation(ProgramObject, AttribTangent, "AttributeTangent");
// Связываем все вместе
GL.LinkProgram(ProgramObject);
// Удаляем ранее созданные шейдеры
GL.DeleteShader(VertexShaderObject);
GL.DeleteShader(FragmentShaderObject);
GL.GetProgram(ProgramObject, GetProgramParameterName.LinkStatus, out temp[0]);
Console.WriteLine("Связывание программ (" + ProgramObject + ") " + ((temp[0] == 1) ? "выполнено." : "НЕ ВЫПОЛНЕНО"));
if (temp[0] != 1) // В случае неудачи при связывании
{
GL.GetProgramInfoLog(ProgramObject, out LogInfo);
Console.WriteLine("Информация:\n" + LogInfo);
}
GL.GetProgram(ProgramObject, GetProgramParameterName.ActiveAttributes, out temp[0]);
Console.WriteLine("Зарегестрировано " + temp[0] + " атрибута. (Должно быть 4: Pos, UV, Normal, Tangent)");
Console.WriteLine("Положение атрибута AttributeTangent: " + GL.GetAttribLocation(ProgramObject, "AttributeTangent"));
Console.WriteLine("Создание шейдера завершено. GL-ошибка: " + GL.GetError());
ImageDDS.LoadFromDisk(TMU0_Filename, false, false, out TMU0_Handle, out TMU0_Target);
Console.WriteLine("Загружен " + TMU0_Filename + " с обработчиком " + TMU0_Handle + " как " + TMU0_Target);
ImageDDS.LoadFromDisk(TMU1_Filename, false, false, out TMU1_Handle, out TMU1_Target);
Console.WriteLine("Загружен " + TMU1_Filename + " с обработчиком " + TMU1_Handle + " как " + TMU1_Target);
Console.WriteLine("Загрузка текстур завершена. GL-ошибка: " + GL.GetError());
Console.WriteLine("");
}
protected override void OnUnload(EventArgs e)
{
GL.DeleteProgram(ProgramObject);
GL.DeleteTextures(1, ref TMU0_Handle);
GL.DeleteTextures(1, ref TMU1_Handle);
base.OnUnload(e);
}
// Обработчик изменения размеров окна графического вывода
protected override void OnResize(EventArgs e)
{
GL.Viewport(0, 0, Width, Height);
GL.MatrixMode(MatrixMode.Projection);
Matrix4 p = Matrix4.CreatePerspectiveFieldOfView(MathHelper.PiOver4, Width / (float)Height, 0.1f, 100.0f);
GL.LoadMatrix(ref p);
GL.MatrixMode(MatrixMode.Modelview);
GL.LoadIdentity();
base.OnResize(e);
}
// Обработчик нажатия на клавиши клавиатуры
protected override void OnKeyDown(OpenTK.Input.KeyboardKeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Keyboard[OpenTK.Input.Key.Escape]) this.Exit();
if (e.Keyboard[OpenTK.Input.Key.Space]) Console.WriteLine("GL: " + GL.GetError());
if (e.Keyboard[OpenTK.Input.Key.Q])
{
MaterialScaleAndBiasAndShininess.X += 0.01f;
Console.WriteLine("Масштаб: " + MaterialScaleAndBiasAndShininess.X);
}
if (e.Keyboard[OpenTK.Input.Key.A])
{
MaterialScaleAndBiasAndShininess.X -= 0.01f;
Console.WriteLine("Масштаб: " + MaterialScaleAndBiasAndShininess.X);
}
if (e.Keyboard[OpenTK.Input.Key.W])
{
MaterialScaleAndBiasAndShininess.Y += 0.01f;
Console.WriteLine("Смещение: " + MaterialScaleAndBiasAndShininess.Y);
}
if (e.Keyboard[OpenTK.Input.Key.S])
{
MaterialScaleAndBiasAndShininess.Y -= 0.01f;
Console.WriteLine("Смещение: " + MaterialScaleAndBiasAndShininess.Y);
}
if (e.Keyboard[OpenTK.Input.Key.E])
{
MaterialScaleAndBiasAndShininess.Z += 1f;
Console.WriteLine("Яркость: " + MaterialScaleAndBiasAndShininess.Z);
}
if (e.Keyboard[OpenTK.Input.Key.D])
{
MaterialScaleAndBiasAndShininess.Z -= 1f;
Console.WriteLine("Яркость: " + MaterialScaleAndBiasAndShininess.Z);
}
}
protected override void OnMouseMove(OpenTK.Input.MouseMoveEventArgs e)
{
base.OnMouseMove(e);
LightPosition.X = (-(this.Width / 2) + e.Mouse.X) / 100f;
LightPosition.Y = ((this.Height / 2) - e.Mouse.Y) / 100f;
EyePos.Y = e.Mouse.Wheel * 0.5f;
}
// Обработчик обновления
protected override void OnUpdateFrame(FrameEventArgs e)
{
base.OnUpdateFrame(e);
}
// Обработчик воспроизведения
protected override void OnRenderFrame(FrameEventArgs e)
{
this.Title = "FPS: " + (1 / e.Time).ToString("0.");
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.UseProgram(ProgramObject);
GL.ActiveTexture(TMU0_Unit);
GL.BindTexture(TMU0_Target, TMU0_Handle);
GL.ActiveTexture(TMU1_Unit);
GL.BindTexture(TMU1_Target, TMU1_Handle);
// Характеристики материала
GL.Uniform1(GL.GetUniformLocation(ProgramObject, "Material_DiffuseAndHeight"), TMU0_UnitInteger);
GL.Uniform1(GL.GetUniformLocation(ProgramObject, "Material_NormalAndGloss"), TMU1_UnitInteger);
GL.Uniform3(GL.GetUniformLocation(ProgramObject, "Material_ScaleBiasShininess"), MaterialScaleAndBiasAndShininess.X, MaterialScaleAndBiasAndShininess.Y, MaterialScaleAndBiasAndShininess.Z);
// Прочие - это векторы
GL.Uniform3(GL.GetUniformLocation(ProgramObject, "Camera_Position"), EyePos.X, EyePos.Y, EyePos.Z);
GL.Uniform3(GL.GetUniformLocation(ProgramObject, "Light_Position"), LightPosition.X, LightPosition.Y, LightPosition.Z);
GL.Uniform3(GL.GetUniformLocation(ProgramObject, "Light_DiffuseColor"), LightDiffuse.X, LightDiffuse.Y, LightDiffuse.Z);
GL.Uniform3(GL.GetUniformLocation(ProgramObject, "Light_SpecularColor"), LightSpecular.X, LightSpecular.Y, LightSpecular.Z);
GL.PushMatrix();
Matrix4 t = Matrix4.LookAt(EyePos, Vector3.Zero, Vector3.UnitY);
GL.MultMatrix(ref t);
GL.Color3(1f, 1f, 1f);
GL.Begin(PrimitiveType.Quads); // Выводим один четырехугольник
{
GL.Normal3(Normal);
GL.VertexAttrib3(AttribTangent, ref Tangent);
GL.MultiTexCoord2(TextureUnit.Texture0, 0f, 1f);
GL.Vertex3(-1.0f, 1.0f, 0.0f);
GL.Normal3(Normal);
GL.VertexAttrib3(AttribTangent, ref Tangent);
GL.MultiTexCoord2(TextureUnit.Texture0, 0f, 0f);
GL.Vertex3(-1.0f, -1.0f, 0.0f);
GL.Normal3(Normal);
GL.VertexAttrib3(AttribTangent, ref Tangent);
GL.MultiTexCoord2(TextureUnit.Texture0, 1f, 0f);
GL.Vertex3(1.0f, -1.0f, 0.0f);
GL.Normal3(Normal);
GL.VertexAttrib3(AttribTangent, ref Tangent);
GL.MultiTexCoord2(TextureUnit.Texture0, 1f, 1f);
GL.Vertex3(1.0f, 1.0f, 0.0f);
}
GL.End();
GL.UseProgram(0);
// Отобразим позицию источника света в виде точки в LightPosition
GL.Begin(PrimitiveType.Points);
{
GL.Color3(LightSpecular);
GL.Vertex3(LightPosition);
}
GL.End();
GL.PopMatrix();
this.SwapBuffers();
}
// Точка входа
[STAThread]
public static void Main()
{
using (T12_GLSL_Parallax mapping = new T12_GLSL_Parallax())
{
mapping.Title = "Swizzled Parallax Mapping";
mapping.Run(30.0, 0.0);
}
}
}
}
namespace TextureLoaders
{
// Нужна подходящая версия OpenGL с поддержкой сжатия текстур (GL 1.5) и кубических карт (GL 1.3)
// Необходимо, чтобы размеры текстуры делились на 4, поскольку DXTn использует блоки 4x4
// Кубическая карта должна быть задана для всех шести сторон куба
static class ImageDDS
{
private const byte HeaderSizeInBytes = 128; // Рабзмер заголовка в байтах
private const uint BitMask = 0x00000007; // биты 00 00 01 11
private static NotImplementedException Unfinished = new NotImplementedException("ERROR: Only 2 Dimensional DXT1/3/5 compressed images for now. 1D/3D Textures may not be compressed according to spec.");
private static bool _IsCompressed;
private static int _Width, _Height, _Depth, _MipMapCount;
private static int _BytesForMainSurface;
private static byte _BytesPerBlock;
private static PixelInternalFormat _PixelInternalFormat;
[Flags] // Описание поверхности
private enum eDDSD : uint
{
CAPS = 0x00000001, // is always present
HEIGHT = 0x00000002, // is always present
WIDTH = 0x00000004, // is always present
PITCH = 0x00000008, // is set if the image is uncompressed
PIXELFORMAT = 0x00001000, // is always present
MIPMAPCOUNT = 0x00020000, // is set if the image contains MipMaps
LINEARSIZE = 0x00080000, // is set if the image is compressed
DEPTH = 0x00800000 // is set for 3D Volume Textures
}
[Flags] // Формат пикселя
private enum eDDPF : uint
{
NONE = 0x00000000, // Not part of DX, added for convenience
ALPHAPIXELS = 0x00000001,
FOURCC = 0x00000004,
RGB = 0x00000040,
RGBA = 0x00000041
}
// Перечень взят из nVidia OpenGL SDK
[Flags] // Типы текстур
private enum eFOURCC : uint
{
UNKNOWN = 0,
DXT1 = 0x31545844,
DXT2 = 0x32545844,
DXT3 = 0x33545844,
DXT4 = 0x34545844,
DXT5 = 0x35545844,
}
[Flags] // dwCaps1
private enum eDDSCAPS : uint
{
NONE = 0x00000000, // not part of DX, added for convenience
COMPLEX = 0x00000008, // should be set for any DDS file with more than one main surface
TEXTURE = 0x00001000, // should always be set
MIPMAP = 0x00400000 // only for files with MipMaps
}
[Flags] // dwCaps2
private enum eDDSCAPS2 : uint
{
NONE = 0x00000000, // not part of DX, added for convenience
CUBEMAP = 0x00000200,
CUBEMAP_POSITIVEX = 0x00000400,
CUBEMAP_NEGATIVEX = 0x00000800,
CUBEMAP_POSITIVEY = 0x00001000,
CUBEMAP_NEGATIVEY = 0x00002000,
CUBEMAP_POSITIVEZ = 0x00004000,
CUBEMAP_NEGATIVEZ = 0x00008000,
CUBEMAP_ALL_FACES = 0x0000FC00,
VOLUME = 0x00200000 // for 3D Textures
}
private static string idString; // 4 байта, должно быть "DDS"
private static UInt32 dwSize; // Size of structure is 124 bytes, 128 including all sub-structs and the header
private static UInt32 dwFlags; // Flags to indicate valid fields
private static UInt32 dwHeight; // Height of the main image in pixels
private static UInt32 dwWidth; // Width of the main image in pixels
private static UInt32 dwPitchOrLinearSize; // For compressed formats, this is the total number of bytes for the main image
private static UInt32 dwDepth; // For volume textures, this is the depth of the volume
private static UInt32 dwMipMapCount; // Total number of levels in the mipmap chain of the main image
// Pixelformat sub-struct, 32 bytes
private static UInt32 pfSize; // Size of Pixelformat structure. This member must be set to 32
private static UInt32 pfFlags; // Flags to indicate valid fields
private static UInt32 pfFourCC; // This is the four-character code for compressed formats
// Capabilities sub-struct, 16 bytes
private static UInt32 dwCaps1; // Always includes DDSCAPS_TEXTURE with more than one main surface DDSCAPS_COMPLEX should also be set
// For cubic environment maps DDSCAPS2_CUBEMAP should be included
// as well as one or more faces of the map (DDSCAPS2_CUBEMAP_POSITIVEX,
// DDSCAPS2_CUBEMAP_NEGATIVEX, DDSCAPS2_CUBEMAP_POSITIVEY,
// DDSCAPS2_CUBEMAP_NEGATIVEY, DDSCAPS2_CUBEMAP_POSITIVEZ, DDSCAPS2_CUBEMAP_NEGATIVEZ)
// For volume textures, DDSCAPS2_VOLUME should be included
private static UInt32 dwCaps2;
// This function will generate, bind and fill a Texture Object with a DXT1/3/5 compressed Texture in .dds Format.
// MipMaps below 4x4 Pixel Size are discarded, because DXTn's smallest unit is a 4x4 block of Pixel data.
// It will set correct MipMap parameters, Filtering, Wrapping and EnvMode for the Texture.
// The only call inside this function affecting OpenGL State is GL.BindTexture();
// The name of the file you wish to load, including path and file extension
// 0 if invalid, otherwise a Texture Object usable with GL.BindTexture()
// 0 if invalid, will output what was loaded (typically Texture1D/2D/3D or Cubemap)
public static void LoadFromDisk(string filename, bool FlipImages, bool Verbose, out uint texturehandle, out TextureTarget dimension)
{
// Начальные установки
dimension = (TextureTarget)0;
ErrorCode GLError = ErrorCode.NoError;
_IsCompressed = false;
_Width = 0;
_Height = 0;
_Depth = 0;
_MipMapCount = 0;
_BytesForMainSurface = 0;
_BytesPerBlock = 0;
_PixelInternalFormat = PixelInternalFormat.Rgba8;
byte[] _RawDataFromFile;
try // Исключение возникнет при ошибках чтения файла
{
_RawDataFromFile = File.ReadAllBytes(@filename);
ConvertDX9Header(ref _RawDataFromFile); // The first 128 Bytes of the file is non-image data
// Поверка всех констант и флагов
if (idString != "DDS " || // magic key
dwSize != 124 || // constant size of struct, never reused
pfSize != 32 || // constant size of struct, never reused
!CheckFlag(dwFlags, (uint)eDDSD.CAPS) || // must know it's caps
!CheckFlag(dwFlags, (uint)eDDSD.PIXELFORMAT) || // must know it's format
!CheckFlag(dwCaps1, (uint)eDDSCAPS.TEXTURE) // must be a Texture
)
throw new ArgumentException("ERROR: File has invalid signature or missing Flags.");
if (CheckFlag(dwFlags, (uint)eDDSD.WIDTH))
_Width = (int)dwWidth;
else
throw new ArgumentException("ERROR: Flag for Width not set.");
if (CheckFlag(dwFlags, (uint)eDDSD.HEIGHT))
_Height = (int)dwHeight;
else
throw new ArgumentException("ERROR: Flag for Height not set.");
if (CheckFlag(dwFlags, (uint)eDDSD.DEPTH) && CheckFlag(dwCaps2, (uint)eDDSCAPS2.VOLUME))
{
dimension = TextureTarget.Texture3D; // image is 3D Volume
_Depth = (int)dwDepth;
throw Unfinished;
}
else
{// Куб или образ 2D
if (CheckFlag(dwCaps2, (uint)eDDSCAPS2.CUBEMAP))
{
dimension = TextureTarget.TextureCubeMap;
_Depth = 6;
}
else
{
dimension = TextureTarget.Texture2D;
_Depth = 1;
}
}
// Устанавливаем флаг при наличии mip-карт
if (CheckFlag(dwCaps1, (uint)eDDSCAPS.MIPMAP) && CheckFlag(dwFlags, (uint)eDDSD.MIPMAPCOUNT))
_MipMapCount = (int)dwMipMapCount; // Образ содержит mip-карты
else
_MipMapCount = 1; // Только один главный образ
// Should never happen
if (CheckFlag(dwFlags, (uint)eDDSD.PITCH) && CheckFlag(dwFlags, (uint)eDDSD.LINEARSIZE))
throw new ArgumentException("ОШИБКА: Одновременно указаны флаги Наклонный и Линейный. Образ не может быть одновременно несжатым и DTXn-сжатым");
// This flag is set if format is uncompressed RGB RGBA etc.
if (CheckFlag(dwFlags, (uint)eDDSD.PITCH))
{
_IsCompressed = false;
throw Unfinished;
}
// This flag is set if format is compressed DXTn.
if (CheckFlag(dwFlags, (uint)eDDSD.LINEARSIZE))
{
_BytesForMainSurface = (int)dwPitchOrLinearSize;
_IsCompressed = true;
}
if (CheckFlag(pfFlags, (uint)eDDPF.FOURCC))
switch ((eFOURCC)pfFourCC)
{
case eFOURCC.DXT1:
_PixelInternalFormat = (PixelInternalFormat)ExtTextureCompressionS3tc.CompressedRgbS3tcDxt1Ext;
_BytesPerBlock = 8;
_IsCompressed = true;
break;
case eFOURCC.DXT3:
_PixelInternalFormat = (PixelInternalFormat)ExtTextureCompressionS3tc.CompressedRgbaS3tcDxt3Ext;
_BytesPerBlock = 16;
_IsCompressed = true;
break;
case eFOURCC.DXT5:
_PixelInternalFormat = (PixelInternalFormat)ExtTextureCompressionS3tc.CompressedRgbaS3tcDxt5Ext;
_BytesPerBlock = 16;
_IsCompressed = true;
break;
default:
throw Unfinished; // Handle uncompressed formats
}
else
throw Unfinished;
if (Verbose) Console.WriteLine("\n" + GetDescriptionFromMemory(filename, dimension));
GL.GenTextures(1, out texturehandle);
GL.BindTexture(dimension, texturehandle);
int Cursor = HeaderSizeInBytes;
// Для кубической карты получаем все mip-карты. Только одна итерация для Texture2D
for (int Slices = 0; Slices < _Depth; Slices++)
{
int trueMipMapCount = _MipMapCount - 1;
int Width = _Width;
int Height = _Height;
for (int Level = 0; Level < _MipMapCount; Level++) // Начинаем с базового образа
{
int BlocksPerRow = (Width + 3) >> 2;
int BlocksPerColumn = (Height + 3) >> 2;
int SurfaceBlockCount = BlocksPerRow * BlocksPerColumn; // DXTn stores Texels in 4x4 blocks, a Color block is 8 Bytes, an Alpha block is 8 Bytes for DXT3/5
int SurfaceSizeInBytes = SurfaceBlockCount * _BytesPerBlock;
// This check must evaluate to false for 2D and Cube maps, or it's impossible to determine MipMap sizes.
if (Verbose && Level == 0 && _IsCompressed && _BytesForMainSurface != SurfaceSizeInBytes)
Console.WriteLine("Warning: Calculated byte-count of main image differs from what was read from file.");
// Skip mipmaps smaller than a 4x4 Pixels block, which is the smallest DXTn unit
if (Width > 2 && Height > 2)
{ // Замечание: при работе с образами, размеры которых не являются степенью числа 2, моогут быть проблемы
byte[] RawDataOfSurface = new byte[SurfaceSizeInBytes];
if (!FlipImages)
{ // Без изменений образа
Array.Copy(_RawDataFromFile, Cursor, RawDataOfSurface, 0, SurfaceSizeInBytes);
}
else
{ // Поворот образа на 180 градусов
for (int sourceColumn = 0; sourceColumn < BlocksPerColumn; sourceColumn++)
{
int targetColumn = BlocksPerColumn - sourceColumn - 1;
for (int row = 0; row < BlocksPerRow; row++)
{
int target = (targetColumn * BlocksPerRow + row) * _BytesPerBlock;
int source = (sourceColumn * BlocksPerRow + row) * _BytesPerBlock + Cursor;
switch (_PixelInternalFormat)
{
case (PixelInternalFormat)ExtTextureCompressionS3tc.CompressedRgbS3tcDxt1Ext:
// Color only
RawDataOfSurface[target + 0] = _RawDataFromFile[source + 0];
RawDataOfSurface[target + 1] = _RawDataFromFile[source + 1];
RawDataOfSurface[target + 2] = _RawDataFromFile[source + 2];
RawDataOfSurface[target + 3] = _RawDataFromFile[source + 3];
RawDataOfSurface[target + 4] = _RawDataFromFile[source + 7];
RawDataOfSurface[target + 5] = _RawDataFromFile[source + 6];
RawDataOfSurface[target + 6] = _RawDataFromFile[source + 5];
RawDataOfSurface[target + 7] = _RawDataFromFile[source + 4];
break;
case (PixelInternalFormat)ExtTextureCompressionS3tc.CompressedRgbaS3tcDxt3Ext:
// Alpha
RawDataOfSurface[target + 0] = _RawDataFromFile[source + 6];
RawDataOfSurface[target + 1] = _RawDataFromFile[source + 7];
RawDataOfSurface[target + 2] = _RawDataFromFile[source + 4];
RawDataOfSurface[target + 3] = _RawDataFromFile[source + 5];
RawDataOfSurface[target + 4] = _RawDataFromFile[source + 2];
RawDataOfSurface[target + 5] = _RawDataFromFile[source + 3];
RawDataOfSurface[target + 6] = _RawDataFromFile[source + 0];
RawDataOfSurface[target + 7] = _RawDataFromFile[source + 1];
// Color
RawDataOfSurface[target + 8] = _RawDataFromFile[source + 8];
RawDataOfSurface[target + 9] = _RawDataFromFile[source + 9];
RawDataOfSurface[target + 10] = _RawDataFromFile[source + 10];
RawDataOfSurface[target + 11] = _RawDataFromFile[source + 11];
RawDataOfSurface[target + 12] = _RawDataFromFile[source + 15];
RawDataOfSurface[target + 13] = _RawDataFromFile[source + 14];
RawDataOfSurface[target + 14] = _RawDataFromFile[source + 13];
RawDataOfSurface[target + 15] = _RawDataFromFile[source + 12];
break;
case (PixelInternalFormat)ExtTextureCompressionS3tc.CompressedRgbaS3tcDxt5Ext:
// Alpha, the first 2 bytes remain
RawDataOfSurface[target + 0] = _RawDataFromFile[source + 0];
RawDataOfSurface[target + 1] = _RawDataFromFile[source + 1];
// extract 3 bits each and flip them
GetBytesFromUInt24(ref RawDataOfSurface, (uint)target + 5, FlipUInt24(GetUInt24(ref _RawDataFromFile, (uint)source + 2)));
GetBytesFromUInt24(ref RawDataOfSurface, (uint)target + 2, FlipUInt24(GetUInt24(ref _RawDataFromFile, (uint)source + 5)));
// Color
RawDataOfSurface[target + 8] = _RawDataFromFile[source + 8];
RawDataOfSurface[target + 9] = _RawDataFromFile[source + 9];
RawDataOfSurface[target + 10] = _RawDataFromFile[source + 10];
RawDataOfSurface[target + 11] = _RawDataFromFile[source + 11];
RawDataOfSurface[target + 12] = _RawDataFromFile[source + 15];
RawDataOfSurface[target + 13] = _RawDataFromFile[source + 14];
RawDataOfSurface[target + 14] = _RawDataFromFile[source + 13];
RawDataOfSurface[target + 15] = _RawDataFromFile[source + 12];
break;
default:
throw new ArgumentException("ERROR: Should have never arrived here! Bad _PixelInternalFormat! Should have been dealt with much earlier.");
}
}
}
}
switch (dimension)
{
case TextureTarget.Texture2D:
GL.CompressedTexImage2D(TextureTarget.Texture2D, Level, _PixelInternalFormat,
Width, Height, 0, SurfaceSizeInBytes, RawDataOfSurface);
break;
case TextureTarget.TextureCubeMap:
GL.CompressedTexImage2D(TextureTarget.TextureCubeMapPositiveX + Slices, Level, _PixelInternalFormat,
Width, Height, 0, SurfaceSizeInBytes, RawDataOfSurface);
break;
default:
throw new ArgumentException("Ошибка: Используйтеп DXT для 2D-образов. Не могу работать с " + dimension);
}
GL.Finish();
int width, height, internalformat, compressed;
switch (dimension)
{
case TextureTarget.Texture1D:
case TextureTarget.Texture2D:
case TextureTarget.Texture3D:
GL.GetTexLevelParameter(dimension, Level, GetTextureParameter.TextureWidth, out width);
GL.GetTexLevelParameter(dimension, Level, GetTextureParameter.TextureHeight, out height);
GL.GetTexLevelParameter(dimension, Level, GetTextureParameter.TextureInternalFormat, out internalformat);
GL.GetTexLevelParameter(dimension, Level, GetTextureParameter.TextureCompressed, out compressed);
break;
case TextureTarget.TextureCubeMap:
GL.GetTexLevelParameter(TextureTarget.TextureCubeMapPositiveX + Slices, Level, GetTextureParameter.TextureWidth, out width);
GL.GetTexLevelParameter(TextureTarget.TextureCubeMapPositiveX + Slices, Level, GetTextureParameter.TextureHeight, out height);
GL.GetTexLevelParameter(TextureTarget.TextureCubeMapPositiveX + Slices, Level, GetTextureParameter.TextureInternalFormat, out internalformat);
GL.GetTexLevelParameter(TextureTarget.TextureCubeMapPositiveX + Slices, Level, GetTextureParameter.TextureCompressed, out compressed);
break;
default:
throw Unfinished;
}
GLError = GL.GetError();
if (Verbose)
Console.WriteLine("GL: " + GLError.ToString() + " Level: " + Level + " DXTn: " + ((compressed == 1) ? "Yes" : "No") + " Frmt:" + (ExtTextureCompressionS3tc)internalformat + " " + width + "*" + height);
if (GLError != ErrorCode.NoError || compressed == 0 || width == 0 || height == 0 || internalformat == 0)
{
GL.DeleteTextures(1, ref texturehandle);
throw new ArgumentException("ERROR: Something went wrong after GL.CompressedTexImage(); Last GL Error: " + GLError.ToString());
}
}
else
if (trueMipMapCount > Level) trueMipMapCount = Level - 1; // The current Level is invalid
Width /= 2;
if (Width < 1) Width = 1;
Height /= 2;
if (Height < 1) Height = 1;
Cursor += SurfaceSizeInBytes;
}
GL.TexParameter(dimension, (TextureParameterName)All.TextureBaseLevel, 0);
GL.TexParameter(dimension, (TextureParameterName)All.TextureMaxLevel, trueMipMapCount);
int TexMaxLevel;
GL.GetTexParameter(dimension, GetTextureParameter.TextureMaxLevel, out TexMaxLevel);
if (Verbose)
Console.WriteLine("Verification: GL: " + GL.GetError().ToString() + " TextureMaxLevel: " + TexMaxLevel + ((TexMaxLevel == trueMipMapCount) ? " (Correct.)" : " (Wrong!)"));
}
GL.TexParameter(dimension, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(dimension, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
GL.TexParameter(dimension, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToBorder);
GL.TexParameter(dimension, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToBorder);
GL.TexEnv(TextureEnvTarget.TextureEnv, TextureEnvParameter.TextureEnvMode, (int)TextureEnvMode.Modulate);
GLError = GL.GetError();
if (GLError != ErrorCode.NoError)
throw new ArgumentException("Ошибка при установки параметров тестуры. GL-ошибка: " + GLError);
return; // Удача
}
catch (Exception e)
{
dimension = (TextureTarget)0;
throw new ArgumentException("ERROR: Exception caught when attempting to load file " + filename + ".\n" + e + "\n" + GetDescriptionFromFile(filename));
}
finally
{
_RawDataFromFile = null;
}
}
private static void ConvertDX9Header(ref byte[] input)
{
UInt32 offset = 0;
idString = GetString(ref input, offset);
offset += 4;
dwSize = GetUInt32(ref input, offset);
offset += 4;
dwFlags = GetUInt32(ref input, offset);
offset += 4;
dwHeight = GetUInt32(ref input, offset);
offset += 4;
dwWidth = GetUInt32(ref input, offset);
offset += 4;
dwPitchOrLinearSize = GetUInt32(ref input, offset);
offset += 4;
dwDepth = GetUInt32(ref input, offset);
offset += 4;
dwMipMapCount = GetUInt32(ref input, offset);
offset += 4;
offset += 4 * 11;
pfSize = GetUInt32(ref input, offset);
offset += 4;
pfFlags = GetUInt32(ref input, offset);
offset += 4;
pfFourCC = GetUInt32(ref input, offset);
offset += 4;
offset += 20;
dwCaps1 = GetUInt32(ref input, offset);
offset += 4;
dwCaps2 = GetUInt32(ref input, offset);
offset += 4;
offset += 4 * 3;
}
// Returns true if the flag is set, false otherwise
private static bool CheckFlag(uint variable, uint flag)
{
return (variable & flag) > 0 ? true : false;
}
private static string GetString(ref byte[] input, uint offset)
{
return "" + (char)input[offset + 0] + (char)input[offset + 1] + (char)input[offset + 2] + (char)input[offset + 3];
}
private static uint GetUInt32(ref byte[] input, uint offset)
{
return (uint)(((input[offset + 3] * 256 + input[offset + 2]) * 256 + input[offset + 1]) * 256 + input[offset + 0]);
}
private static uint GetUInt24(ref byte[] input, uint offset)
{
return (uint)((input[offset + 2] * 256 + input[offset + 1]) * 256 + input[offset + 0]);
}
private static void GetBytesFromUInt24(ref byte[] input, uint offset, uint splitme)
{
input[offset + 0] = (byte)(splitme & 0x000000ff);
input[offset + 1] = (byte)((splitme & 0x0000ff00) >> 8);
input[offset + 2] = (byte)((splitme & 0x00ff0000) >> 16);
return;
}
// DXT5 Alpha block flipping, inspired by code from Evan Hart (nVidia SDK)
private static uint FlipUInt24(uint inputUInt24)
{
byte[][] ThreeBits = new byte[2][];
for (int i = 0; i < 2; i++) ThreeBits[i] = new byte[4];
// Extract 3 bits each into the array
ThreeBits[0][0] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[0][1] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[0][2] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[0][3] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[1][0] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[1][1] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[1][2] = (byte)(inputUInt24 & BitMask);
inputUInt24 >>= 3;
ThreeBits[1][3] = (byte)(inputUInt24 & BitMask);
// Stuff 8x 3bits into 3 bytes
uint Result = 0;
Result = Result | (uint)(ThreeBits[1][0] << 0);
Result = Result | (uint)(ThreeBits[1][1] << 3);
Result = Result | (uint)(ThreeBits[1][2] << 6);
Result = Result | (uint)(ThreeBits[1][3] << 9);
Result = Result | (uint)(ThreeBits[0][0] << 12);
Result = Result | (uint)(ThreeBits[0][1] << 15);
Result = Result | (uint)(ThreeBits[0][2] << 18);
Result = Result | (uint)(ThreeBits[0][3] << 21);
return Result;
}
private static string GetDescriptionFromFile(string filename)
{
return "\n--> Header of " + filename +
"\nID: " + idString +
"\nSize: " + dwSize +
"\nFlags: " + dwFlags + " (" + (eDDSD)dwFlags + ")" +
"\nHeight: " + dwHeight +
"\nWidth: " + dwWidth +
"\nPitch: " + dwPitchOrLinearSize +
"\nDepth: " + dwDepth +
"\nMipMaps: " + dwMipMapCount +
"\n\n---PixelFormat---" + filename +
"\nSize: " + pfSize +
"\nFlags: " + pfFlags + " (" + (eDDPF)pfFlags + ")" +
"\nFourCC: " + pfFourCC + " (" + (eFOURCC)pfFourCC + ")" +
"\n\n---Capabilities---" + filename +
"\nCaps1: " + dwCaps1 + " (" + (eDDSCAPS)dwCaps1 + ")" +
"\nCaps2: " + dwCaps2 + " (" + (eDDSCAPS2)dwCaps2 + ")";
}
private static string GetDescriptionFromMemory(string filename, TextureTarget Dimension)
{
return "\nFile: " + filename +
"\nDimension: " + Dimension +
"\nSize: " + _Width + " * " + _Height + " * " + _Depth +
"\nCompressed: " + _IsCompressed +
"\nBytes for Main Image: " + _BytesForMainSurface +
"\nMipMaps: " + _MipMapCount;
}
}
}