Список работ

C#, OpenGL, OpenTK: шейдер с иллюзией выступов и впадин

Содержание

Введение

Приводятся шейдеры вершин и фрагментов, позволяющие наложить текстуру, при наблюдении которой возникает иллюзия выступов и углублений (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-карты первого и восьмого уровней.

Mip-карты первого и восьмого уровней

Рис. 2. Образ для текстуры TMU0_Unit (mip-уровни 1 и 8)

На рис. 3 показаны свойства файла swizzledRockDiffuseHeight.dds для первого mip-уровня.

Свойства swizzledRockDiffuseHeight.dds

Рис. 3. Свойства swizzledRockDiffuseHeight.dds (выбран mip-уровень 1)

Файл swizzledRockNormalGloss.dds содержит карты нормалей и зеркально отражаемого света.
Файл swizzledRockNormalGloss.dds хранит 9 mip-карт, первая и девятая показаны на рис. 4.

Mip-карты первого и девятого уровней

Рис. 4. Образ для текстуры TMU1_Unit (mip-уровни 1 и 9)

На рис. 5 показаны свойства файла swizzledRockNormalGloss.dds для девятого mip-уровня.

Свойства swizzledRockNormalGloss.dds

Рис. 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).

Вывод полигона с Parallax mapping

В приводимой ниже программе изображение управляется в результате изменения следующих параметров:

Первый параметр изменяется в результате нажатий на клавиши Q и A, второй - W и S, на третий влияют клавиши E и D.
На рис. 6 показан квадрат параметрами масштаб и смещение, равными 0.7, и с яркостью 1.0.

Измененным вектор Material_ScaleBiasShininess

Рис. 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;
        }
    }
}

Список работ

Рейтинг@Mail.ru