Загрузка 3D моделей на OpenGL и WebGL

Здесь выкладываем код только на чистом шейдерном OpenGL/GLSL и WebGL/GLSL. Никаких движков типа: Babylon.js, Three.js, Unity, Unreal Engine, CryEngine и т.д.

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

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

Примеры на C# используют библиотеку OpenTK. OpenTK ставится из NuGet и позволяет создавать окно из консольного проекта. Проект с OpenTK откроется в VS для Windows, в VS для Mac и в MonoDevelop для Linux. А так же можно поставить из NuGet контрол OpenTK.GLControl, который позволит сочетать стандартные GUI-элементы WinForms и WPF с OpenGL графикой. Порт уроков из learnopengl.com на OpenTK на официальном сайте: https://opentk.net/learn/index.html

На данный момент я изучаю glTF. Это самый передовой и самый популярный формат - это glTF на сегодняшний. Он позволяет хранить информацию более удобно для парсинга и загрузки в память видео карты, в VBO для шейдерных OpenGL и WebGL. Формат разработан Khronos Group.

Официальный туториал

Большая картинка для обзора glTF

Я когда-то долго мучился с импортом моделей в Three.js.

Не знаю уж в импортерах Three.js дело было или в самих моделях с OpenGameArt и т.п., но постоянно то не всё загружалось, то были глюки при отображении, особенно с анимацией. :crazy_face:

Например как тут внизу https://github.com/AlexP11223/Three.js_GuardedCastle
или https://stackoverflow.com/questions/33898816/three-js-exports-blender-model-without-texture

1 Симпатия

Я в октябре мучился с Three.js, чтобы загрузить модель с помощью GLTFLoader, вот тема: Is AMD compilation old and Three.js does not support it? Я решил, что лучше использовать Babylon.js Этот движок был переписан с нуля на TypeScript и поэтому отлично с ним дружит, как и с JavaScript. На BJS я лекго загрузил анимационную модель в песочницу на TypeScript: https://next.plnkr.co/edit/WEMBjkkJJbHgVPVJ?preview Огромный минус Three.js, что он не может работать нормально с TS, особенно в песочнице. Проблема с OrbitControl в песочнице тоже не решена из коробки, а решена с помощью левого модуля: Usage OrbitControls in TypeScript on Playground Вместо Three.js я решил использовать только Babylon.js, потому что он нормально работает с TS.

Загрузка модели с анимацией с помощью Babylon.js и TypeScript из формата GLB. Анимация открывания двери

Запустить и посмотреть код в песочнице Можете залогиниться, сделать копию, нажав кнопку “Fork”, и экспериментировать с кодом, сохраняя ссылку на кнопку “Save”, чтобы поделиться.

DoorAnimation

Я добавил в топик темы поясняющую информацию:

В интернете я не находил более доходчивого объяснения структуры .dae (Collada) формата, чем это сделал ThinMatrix (Карл) на данном видео: Tutorial #4: Collada (.dae) Format

Загрузка логотипа Mitsubishi из Collada (.dae) формата с помощью языка XPath. XPath - язык запросов к XML. Логотип был создан с помощью этого скрипта на Python в Blender. Рисуется логотип с помощью OpenGL 3 и GLSL из C# проекта. Окно создаётся из консольного проекта с помощью библиотеки OpenTK, которая ставится через NuGet. Порт уроков из learnopengl.com на OpenTK на официальном сайте: https://opentk.net/learn/index.html

Модель логотипа в формате Collada (.dae): Logo.zip (979 Байт)

Функция, которая загружает модель с помощью XPath:

        private void LoadData(string path, out float[] vertices)
        {
            XmlDocument xml = new XmlDocument();
            xml.Load(path);
 
            XmlNamespaceManager xnm = new XmlNamespaceManager(xml.NameTable);
            xnm.AddNamespace("a", "http://www.collada.org/2005/11/COLLADASchema");
 
            XmlElement root = xml.DocumentElement;
            XmlNode pNode = root.SelectSingleNode("//a:p", xnm);
            int[] p = Array.ConvertAll(pNode.InnerText.Split(new char[] { ' ' }), int.Parse);
 
            XmlNode posNode = root.SelectSingleNode("//a:float_array[substring(@id, string-length(@id) - string-length('mesh-positions-array') + 1) = 'mesh-positions-array']", xnm);
            float[] positions = Array.ConvertAll(posNode.InnerText.Split(new char[] { ' ' }), float.Parse);
 
            vertices = new float[3 * p.Length / 2];
            int triangleIndex = 0;
            for (int i = 0; i < p.Length; i++)
            {
                if (i % 2 == 0)
                {
                    vertices[triangleIndex++] = positions[p[i] * 3];
                    vertices[triangleIndex++] = positions[p[i] * 3 + 1];
                    vertices[triangleIndex++] = positions[p[i] * 3 + 2];
                }
            }

Весь код:

Program.cs

using System;
using OpenTK.Graphics.OpenGL;
using OpenTK.Graphics;
using OpenTK;
using System.Xml;
 
namespace MitsubishiLogoFromDae
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var window = new Window())
            {
                window.Title = "Mitsubishi";
                window.Run();
            }
        }
    }
 
    class Window : GameWindow
    {
        private Matrix4 _projMatrix;
        private Matrix4 _modelMatrix;
        private Matrix4 _mpMatrix;
        private int _uMPMatrixLocation;
        private int _amountOfVertices = 0;
 
        public Window() : base(250, 250, new GraphicsMode(32, 0, 0, 8)) { }
 
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            var vShaderSource =
                @"
                    #version 130
 
                    in vec3 aPosition;
                    uniform mat4 uMPMatrix;
 
                    void main()
                    {
                        gl_Position = uMPMatrix * vec4(aPosition, 1.0);
                    }
                ";
            var fShaderSource =
                @"
                    #version 130
                    precision mediump float;
 
                    out vec4 fragColor;
 
                    void main()
                    {
                        fragColor = vec4(0.0);
                    }
                ";
            var vShader = GL.CreateShader(ShaderType.VertexShader);
            GL.ShaderSource(vShader, vShaderSource);
            GL.CompileShader(vShader);
            var fShader = GL.CreateShader(ShaderType.FragmentShader);
            GL.ShaderSource(fShader, fShaderSource);
            GL.CompileShader(fShader);
            var program = GL.CreateProgram();
            GL.AttachShader(program, vShader);
            GL.AttachShader(program, fShader);
            GL.LinkProgram(program);
            GL.UseProgram(program);
 
            int vbo;
            GL.CreateBuffers(1, out vbo);
 
            float[] positions;
            LoadData("Assets/Models/Logo.dae", out positions);
            _amountOfVertices = positions.Length / 3;
 
            GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
            GL.BufferData(BufferTarget.ArrayBuffer, sizeof(float) * positions.Length, positions, BufferUsageHint.StaticDraw);
            var aPositionLocation = GL.GetAttribLocation(program, "aPosition");
            GL.VertexAttribPointer(aPositionLocation, 3, VertexAttribPointerType.Float, false, 0, 0);
            GL.EnableVertexAttribArray(aPositionLocation);
 
            _uMPMatrixLocation = GL.GetUniformLocation(program, "uMPMatrix");
 
            GL.ClearColor(1f, 1f, 1f, 1f);
        }
 
        protected override void OnRenderFrame(FrameEventArgs e)
        {
            base.OnRenderFrame(e);
            GL.Clear(ClearBufferMask.ColorBufferBit);
 
            _modelMatrix =
                Matrix4.CreateScale(5f, 5f, 1f) *
                Matrix4.CreateTranslation(0f, 0f, -1f);
            _mpMatrix = _modelMatrix * _projMatrix;
            GL.UniformMatrix4(_uMPMatrixLocation, false, ref _mpMatrix);
            GL.DrawArrays(PrimitiveType.Triangles, 0, _amountOfVertices);
 
            SwapBuffers();
        }
 
        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            GL.Viewport(0, 0, Width, Height);
 
            float aspect = (float)Width / Height;
            float worldWidth = aspect * 20f;
            _projMatrix = Matrix4.CreateOrthographic(worldWidth, 20f, 100f, -100f);
        }
 
        private void LoadData(string path, out float[] vertices)
        {
            XmlDocument xml = new XmlDocument();
            xml.Load(path);
 
            XmlNamespaceManager xnm = new XmlNamespaceManager(xml.NameTable);
            xnm.AddNamespace("a", "http://www.collada.org/2005/11/COLLADASchema");
 
            XmlElement root = xml.DocumentElement;
            XmlNode pNode = root.SelectSingleNode("//a:p", xnm);
            int[] p = Array.ConvertAll(pNode.InnerText.Split(new char[] { ' ' }), int.Parse);
 
            XmlNode posNode = root.SelectSingleNode("//a:float_array[substring(@id, string-length(@id) - string-length('mesh-positions-array') + 1) = 'mesh-positions-array']", xnm);
            float[] positions = Array.ConvertAll(posNode.InnerText.Split(new char[] { ' ' }), float.Parse);
 
            vertices = new float[3 * p.Length / 2];
            int triangleIndex = 0;
            for (int i = 0; i < p.Length; i++)
            {
                if (i % 2 == 0)
                {
                    vertices[triangleIndex++] = positions[p[i] * 3];
                    vertices[triangleIndex++] = positions[p[i] * 3 + 1];
                    vertices[triangleIndex++] = positions[p[i] * 3 + 2];
                }
            }
        }
    }
}

MitsubishiLogoFromDae_OpenTkOpenGL30CSharp

Если там не 1.0 XPath, то можно ends-with(@id, 'mesh-positions-array')

1 Симпатия