Initial push

This commit is contained in:
ti_mo
2026-01-09 15:16:22 +01:00
commit b54b927959
34 changed files with 3060 additions and 0 deletions
@@ -0,0 +1,3 @@
<Solution>
<Project Path="WpfOpenGLSubwindowingClasses/WpfOpenGLSubwindowingClasses.csproj" />
</Solution>
@@ -0,0 +1,141 @@
using OpenTK.Mathematics;
using System;
using System.Windows;
using System.Windows.Input;
namespace WpfOpenGLSubwindowingClasses.Camera
{
public static class OrbitCamera
{
public static Matrix4 View;
public static Matrix4 Projection;
public static Matrix4 ViewMatrix;
public static Vector3 Target = Vector3.Zero;
public static float OrbitRadius;
public static double Yaw = -90.0f;
public static double Pitch = 0.0f;
public static float MouseSensitivity = 0.1f;
public static float ScrollSpeed = 0.1f;
public static float Zoom = 45.0f;
public static int Width, Height;
public static float Near = 0.1f, Far = 1000f;
public static Vector3 Position = Vector3.Zero;
public static Vector3 Front = new Vector3(0.0f, 0.0f, -1.0f);
public static Vector3 Up = Vector3.UnitY;
public static Vector3 Right = Vector3.UnitX;
public static Vector3 WorldUp = Vector3.UnitY;
private static double _lastX = 0.0f;
private static double _lastY = 0.0f;
public static float MinOrbitRadius = 0.1f;
public static float MaxOrbitRadius = 10000f;
public static bool IsLeftMousePressed;
public static void Initialize(Vector3 initialPosition, Vector3 worldUp)
{
Target = Vector3.Zero;
WorldUp = worldUp;
OrbitRadius = (initialPosition - Target).Length;
if (OrbitRadius < 0.1f) OrbitRadius = 0.1f;
var toCamera = Vector3.Normalize(initialPosition - Target);
Pitch = MathHelper.RadiansToDegrees(MathF.Asin(toCamera.Y));
Pitch = Math.Clamp(Pitch, -89.0f, 89.0f);
Yaw = MathHelper.RadiansToDegrees(MathF.Atan2(toCamera.Z, toCamera.X));
RecalculateCameraPositionAndVectors();
}
public static void RecalculateCameraPositionAndVectors()
{
float yawRad = MathHelper.DegreesToRadians((float)Yaw);
float pitchRad = MathHelper.DegreesToRadians((float)Pitch);
var dir = new Vector3(
MathF.Cos(yawRad) * MathF.Cos(pitchRad),
MathF.Sin(pitchRad),
MathF.Sin(yawRad) * MathF.Cos(pitchRad)
);
Position = Target - dir * OrbitRadius;
Front = Vector3.Normalize(Target - Position);
Right = Vector3.Normalize(Vector3.Cross(Front, WorldUp));
Up = Vector3.Normalize(Vector3.Cross(Right, Front));
UpdateView();
}
public static void UpdateView() => View = Matrix4.LookAt(Position, Target, Up);
public static void UpdateProjection(float fov, float aspectRatio) => Projection = Matrix4.CreatePerspectiveFieldOfView(fov, aspectRatio, Near, Far);
public static void MouseDownEvent(object _, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed) IsLeftMousePressed = true;
e.Handled = true;
}
public static void MouseMoveEvent(object sender, MouseEventArgs e)
{
if (sender is not IInputElement control) return;
var point = e.GetPosition(control);
if (IsLeftMousePressed) ProcessMouseMovement(point.X - _lastX, point.Y - _lastY);
_lastX = point.X;
_lastY = point.Y;
e.Handled = true;
}
public static void MouseUpEvent(object _, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released) IsLeftMousePressed = false;
e.Handled = true;
}
public static void MouseWheelEvent(object _, MouseWheelEventArgs e)
{
ProcessMouseScroll(e.Delta);
e.Handled = true;
}
public static void MouseLostFocusEvent(object _, RoutedEventArgs __) => IsLeftMousePressed = false;
private static void ProcessMouseMovement(double xoff, double yoff, bool constrainPitch = true)
{
xoff *= MouseSensitivity;
yoff *= MouseSensitivity;
Yaw += xoff;
Pitch -= yoff;
if (Yaw > 360.0) Yaw -= 360.0;
else if (Yaw < 0.0) Yaw += 360.0;
if (constrainPitch)
{
if (Pitch > 89.0f) Pitch = 89.0f;
if (Pitch < -89.0f) Pitch = -89.0f;
}
RecalculateCameraPositionAndVectors();
}
private static void ProcessMouseScroll(float yoff)
{
float steps = yoff / 120f;
OrbitRadius = Math.Clamp(OrbitRadius - steps * ScrollSpeed, MinOrbitRadius, MaxOrbitRadius);
RecalculateCameraPositionAndVectors();
}
}
}
@@ -0,0 +1,70 @@
using Assimp;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
namespace WpfOpenGLSubwindowingClasses.ModelHandling;
public sealed class AxisLines : IDisposable
{
private int _vao, _vbo, _count;
public void Create(float length = 1f)
{
float L = length;
float[] data = new float[]
{
// X (red)
0,0,0, 1,0,0, L,0,0, 1,0,0,
// Y (green)
0,0,0, 0,1,0, 0,L,0, 0,1,0,
// Z (blue)
0,0,0, 0,0,1, 0,0,-L, 0,0,1,
};
_count = data.Length / 6;
_vao = GL.GenVertexArray();
_vbo = GL.GenBuffer();
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
GL.BufferData(BufferTarget.ArrayBuffer, data.Length * sizeof(float), data, BufferUsageHint.StaticDraw);
int stride = 6 * sizeof(float);
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, 0);
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, 3 * sizeof(float));
GL.BindVertexArray(0);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
}
public void Draw(Shader.Shader shader, Matrix4 view, Matrix4 proj, Matrix4 model, float lineWidth = 2f, bool depthTest = true)
{
if (_vao == 0) return;
if (!depthTest) GL.Disable(EnableCap.DepthTest);
GL.LineWidth(lineWidth);
shader.Use();
shader.SetMatrix4("view", view);
shader.SetMatrix4("proj", proj);
shader.SetMatrix4("model", model);
GL.BindVertexArray(_vao);
GL.DrawArrays(OpenTK.Graphics.OpenGL4.PrimitiveType.Lines, 0, _count);
GL.BindVertexArray(0);
if (!depthTest) GL.Enable(EnableCap.DepthTest);
GL.LineWidth(1f);
}
public void Dispose()
{
if (_vbo != 0) GL.DeleteBuffer(_vbo);
if (_vao != 0) GL.DeleteVertexArray(_vao);
_vbo = _vao = 0;
}
}
@@ -0,0 +1,160 @@
using Assimp;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using PrimitiveType = OpenTK.Graphics.OpenGL4.PrimitiveType;
namespace WpfOpenGLSubwindowingClasses.ModelHandling
{
public sealed class Mesh : IDisposable
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Vertex(Vector3 pos, Vector3 normal, Vector3 color, Vector2 uv)
{
public Vector3 Position = pos;
public Vector3 Normal = normal;
public Vector3 Color = color;
public Vector2 TexCoord = uv;
}
public string Name { get; set; } = string.Empty;
public Vertex[] Vertices { get; set; } = Array.Empty<Vertex>();
public int[] Indices { get; set; } = Array.Empty<int>();
public string? DiffuseTexturePath { get; set; }
public int DiffuseTextureHandle { get; set; } = 0;
public string? SpecularTexturePath { get; set; }
public int SpecularTextureHandle { get; set; } = 0;
public int MaterialIndex { get; set; } = -1;
private int _vao;
private int _vbo;
private int _ebo;
public Color4 AmbientColor { get; set; } = new(0.2f, 0.2f, 0.2f, 1.0f);
public Color4 DiffuseColor { get; set; } = new(0.8f, 0.8f, 0.8f, 1.0f);
public Color4 SpecularColor { get; set; } = new(0.0f, 0.0f, 0.0f, 1.0f);
public float Shininess { get; set; } = 32.0f;
public bool HasGpuBuffers => _vao != 0;
public void CreateGpuBuffers()
{
if (HasGpuBuffers || Vertices.Length == 0 || Indices.Length == 0)
return;
_vao = GL.GenVertexArray();
_vbo = GL.GenBuffer();
_ebo = GL.GenBuffer();
GL.BindVertexArray(_vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
GL.BufferData(BufferTarget.ArrayBuffer, Vertices.Length * Unsafe.SizeOf<Vertex>(), Vertices, BufferUsageHint.StaticDraw);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, _ebo);
GL.BufferData(BufferTarget.ElementArrayBuffer, Indices.Length * sizeof(int), Indices, BufferUsageHint.StaticDraw);
int stride = Unsafe.SizeOf<Vertex>();
var posOff = (int)Marshal.OffsetOf<Vertex>(nameof(Vertex.Position));
var nrmOff = (int)Marshal.OffsetOf<Vertex>(nameof(Vertex.Normal));
var colOff = (int)Marshal.OffsetOf<Vertex>(nameof(Vertex.Color));
var uvOff = (int)Marshal.OffsetOf<Vertex>(nameof(Vertex.TexCoord));
// position (vec3) @ location 0
GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, posOff);
// normal (vec3) @ location 1
GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, nrmOff);
// color (vec3) @ location 2
GL.EnableVertexAttribArray(2);
GL.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, colOff);
// texcoord (vec2) @ location 3
GL.EnableVertexAttribArray(3);
GL.VertexAttribPointer(3, 2, VertexAttribPointerType.Float, false, stride, uvOff);
GL.BindVertexArray(0);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
}
public void Draw(Shader.Shader shader, Matrix4 M, Vector3 translation, OpenTK.Mathematics.Quaternion rotation, Vector3 scale, PrimitiveType? drawModeOverride = null)
{
if (!HasGpuBuffers) return;
if (drawModeOverride == null) drawModeOverride = PrimitiveType.Triangles;
var T = Matrix4.CreateTranslation(translation);
var R = Matrix4.CreateFromQuaternion(rotation);
var S = Matrix4.CreateScale(scale);
// activate shader in beforehand -> activation call each mesh is redundant
shader.SetMatrix4("translation", T);
shader.SetMatrix4("rotation", R);
shader.SetMatrix4("scale", S);
shader.SetMatrix4("model", M);
shader.SetVector4("material_ambient", (Vector4)AmbientColor);
shader.SetVector4("material_diffuse", (Vector4)DiffuseColor);
shader.SetVector4("material_specular", (Vector4)SpecularColor);
shader.SetFloat("material_shininess", Shininess);
// Textur-Handling
bool hasDiffuseTexture = DiffuseTextureHandle != 0;
shader.SetBool("hasDiffuseTexture", hasDiffuseTexture);
if (hasDiffuseTexture)
{
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, DiffuseTextureHandle);
shader.SetInt("diffuse0", 0);
}
else
{
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, 0);
}
// Specular Texture Handling
bool hasSpecularTexture = SpecularTextureHandle != 0;
shader.SetBool("hasSpecularTexture", hasSpecularTexture);
if (hasSpecularTexture)
{
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(TextureTarget.Texture2D, SpecularTextureHandle);
shader.SetInt("specular0", 1);
}
else
{
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(TextureTarget.Texture2D, 0);
}
GL.BindVertexArray(_vao);
GL.DrawElements(drawModeOverride.Value, Indices.Length, DrawElementsType.UnsignedInt, 0);
GL.BindVertexArray(0);
// Opt: unbind tex
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, 0);
GL.ActiveTexture(TextureUnit.Texture1);
GL.BindTexture(TextureTarget.Texture2D, 0);
}
public void Dispose()
{
if (_vao != 0) { GL.DeleteVertexArray(_vao); _vao = 0; }
if (_vbo != 0) { GL.DeleteBuffer(_vbo); _vbo = 0; }
if (_ebo != 0) { GL.DeleteBuffer(_ebo); _ebo = 0; }
if (DiffuseTextureHandle != 0) { GL.DeleteTexture(DiffuseTextureHandle); DiffuseTextureHandle = 0; }
if (SpecularTextureHandle != 0) { GL.DeleteTexture(SpecularTextureHandle); SpecularTextureHandle = 0; }
}
}
}
@@ -0,0 +1,112 @@
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace WpfOpenGLSubwindowingClasses.ModelHandling
{
public class Model(string path, List<Mesh> meshes) : IDisposable
{
public string Path { get; set; } = path;
public List<Mesh> Meshes { get; set; } = meshes;
public OpenTK.Mathematics.Vector3 CenterOffset { get; private set; } = OpenTK.Mathematics.Vector3.Zero;
public Matrix4 ModelMatrix { get; private set; } = Matrix4.Identity;
public OpenTK.Mathematics.Vector3 Translation { get; set; } = OpenTK.Mathematics.Vector3.Zero;
public OpenTK.Mathematics.Quaternion Rotation { get; set; } = OpenTK.Mathematics.Quaternion.Identity;
public OpenTK.Mathematics.Vector3 Scale { get; set; } = OpenTK.Mathematics.Vector3.One;
public void ComputeCenterOffset(OpenTK.Mathematics.Vector3 target = default)
{
var (min, max) = ComputeGlobalBounds();
var currentCenter = (min + max) * 0.5f;
CenterOffset = target - currentCenter;
// ModelMatrix = Matrix4.CreateTranslation(CenterOffset);
Translation = CenterOffset;
}
public void CenterModelAroundVec(OpenTK.Mathematics.Vector3? targetCenter = null)
{
var c = OpenTK.Mathematics.Vector3.Zero;
if (targetCenter is not null) c = targetCenter.Value;
var (min, max) = ComputeGlobalBounds();
var currentCenter = (min + max) * 0.5f;
var offset = c - currentCenter;
foreach (var mesh in Meshes)
{
var verts = mesh.Vertices;
for (int i = 0; i < verts.Length; i++)
{
var v = verts[i];
verts[i] = new Mesh.Vertex(v.Position + offset, v.Normal, v.Color, v.TexCoord);
}
mesh.Vertices = verts;
}
}
public (OpenTK.Mathematics.Vector3 min, OpenTK.Mathematics.Vector3 max) ComputeGlobalBounds()
{
var min = new OpenTK.Mathematics.Vector3(float.MaxValue);
var max = new OpenTK.Mathematics.Vector3(float.MinValue);
foreach (var mesh in Meshes)
{
foreach (var v in mesh.Vertices)
{
min = OpenTK.Mathematics.Vector3.ComponentMin(min, v.Position);
max = OpenTK.Mathematics.Vector3.ComponentMax(max, v.Position);
}
}
return (min, max);
}
public void ApplyUniformScale(float? s = null)
{
if (s == null) s = ComputeScaleToMaxExtent(this, 5f);
if (s <= 0f) return;
foreach (var mesh in Meshes)
{
var verts = mesh.Vertices;
for (int i = 0; i < verts.Length; i++)
{
var v = verts[i];
verts[i] = new Mesh.Vertex(v.Position * (float)s, v.Normal, v.Color, v.TexCoord);
}
mesh.Vertices = verts;
}
}
static float ComputeScaleToMaxExtent(Model model, float targetMaxExtent)
{
var (min, max) = model.ComputeGlobalBounds();
var extent = max - min;
float maxExtent = Math.Max(extent.X, Math.Max(extent.Y, extent.Z));
if (maxExtent <= 0f) return 1f;
return targetMaxExtent / maxExtent;
}
public void Draw(Shader.Shader shader, PrimitiveType? drawModeOverride = null)
{
if (drawModeOverride == null) drawModeOverride = PrimitiveType.Triangles;
foreach (var mesh in Meshes) mesh.Draw(shader, Matrix4.Identity, Translation, Rotation, Scale, drawModeOverride);
}
public void Dispose()
{
foreach(var mesh in Meshes)
{
mesh.Dispose();
}
}
}
}
@@ -0,0 +1,309 @@
using Assimp;
using Assimp.Configs;
using OpenTK.Mathematics;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Numerics;
using System.Windows.Documents;
using Vector2 = OpenTK.Mathematics.Vector2;
using Vector3 = OpenTK.Mathematics.Vector3;
namespace WpfOpenGLSubwindowingClasses.ModelHandling
{
public sealed class ModelLoader(bool logging = false, bool logToDebug = false)
{
public bool Logging { get; set; } = logging;
public bool LogToDebug { get; set; } = logToDebug;
public Model Load(string objPath, bool loadTextures = true)
{
if (string.IsNullOrWhiteSpace(objPath))
throw new ArgumentException("Pfad ist leer.", nameof(objPath));
if (!File.Exists(objPath))
throw new FileNotFoundException("Obj-Datei nicht gefunden.", objPath);
string baseDir = Path.GetDirectoryName(objPath)!;
var ctx = new AssimpContext();
ctx.SetConfig(new Assimp.Configs.FBXImportCamerasConfig(false));
ctx.SetConfig(new NormalSmoothingAngleConfig(60.0f));
var flags =
PostProcessSteps.Triangulate |
PostProcessSteps.JoinIdenticalVertices |
PostProcessSteps.GenerateSmoothNormals |
PostProcessSteps.CalculateTangentSpace |
PostProcessSteps.FlipUVs |
PostProcessSteps.ImproveCacheLocality |
PostProcessSteps.OptimizeMeshes |
PostProcessSteps.PreTransformVertices
;
Scene? scene = null;
try
{
/*
var mmf = MemoryMappedFile.CreateFromFile(objPath);
var stream = mmf.CreateViewStream();
scene = ctx.ImportFileFromStream(stream, flags);
*/
scene = ctx.ImportFile(objPath, flags);
}
catch (Exception e)
{
Debug.WriteLine("Assimp Importer Failed: " + e);
}
if (scene == null || scene.MeshCount == 0)
throw new InvalidOperationException("Import has no meshes.");
var result = new List<Mesh>(scene.MeshCount);
for (int m = 0; m < scene.MeshCount; m++)
{
var aMesh = scene.Meshes[m];
var mesh = new Mesh
{
Name = string.IsNullOrEmpty(aMesh.Name) ? $"Mesh_{m}" : aMesh.Name,
MaterialIndex = aMesh.MaterialIndex
};
// Vertices
var vertices = new Mesh.Vertex[aMesh.VertexCount];
for (int i = 0; i < aMesh.VertexCount; i++)
{
var pos = ToVec3(aMesh.Vertices[i]);
var normal = aMesh.HasNormals ? ToVec3(aMesh.Normals[i]) : Vector3.UnitY;
Vector2 uv = Vector2.Zero;
if (aMesh.HasTextureCoords(0))
{
var t = aMesh.TextureCoordinateChannels[0][i];
uv = new Vector2(t.X, t.Y);
}
Vector3 vertexColor = Vector3.One;
if (aMesh.HasVertexColors(0))
{
var assimpColor = aMesh.VertexColorChannels[0][i];
vertexColor = new Vector3(assimpColor.X, assimpColor.Y, assimpColor.Z);
}
vertices[i] = new Mesh.Vertex(pos, normal, vertexColor, uv);
}
// Indices
var indices = new List<int>(aMesh.FaceCount * 3);
for (int f = 0; f < aMesh.FaceCount; f++)
{
var face = aMesh.Faces[f];
if (face.IndexCount == 3)
{
indices.Add(face.Indices[0]);
indices.Add(face.Indices[1]);
indices.Add(face.Indices[2]);
}
else if (face.IndexCount > 3)
{
for (int k = 1; k < face.IndexCount - 1; k++)
{
indices.Add(face.Indices[0]);
indices.Add(face.Indices[k]);
indices.Add(face.Indices[k + 1]);
}
}
}
mesh.Vertices = vertices;
mesh.Indices = indices.ToArray();
// Material
if (aMesh.MaterialIndex >= 0 && aMesh.MaterialIndex < scene.MaterialCount)
{
var mat = scene.Materials[aMesh.MaterialIndex];
// Diffuse (Kd)
if (mat.HasColorDiffuse)
{
var c = mat.ColorDiffuse;
mesh.DiffuseColor = new(c.X, c.Y, c.Z, c.W);
Log($"Material '{mat.Name}' Diffuse Color (Kd): {mesh.DiffuseColor}");
}
else
{
Log($"Material '{mat.Name}' has no Diffuse Color (Kd), using default.");
}
// Ambient (Ka)
if (mat.HasColorAmbient)
{
var c = mat.ColorAmbient;
mesh.AmbientColor = new(c.X, c.Y, c.Z, c.W);
Log($"Material '{mat.Name}' Ambient Color (Ka): {mesh.AmbientColor}");
}
else
{
Log($"Material '{mat.Name}' has no Ambient Color (Ka), using default.");
}
// Specular (Ks)
if (mat.HasColorSpecular)
{
var c = mat.ColorSpecular;
mesh.SpecularColor = new(c.X, c.Y, c.Z, c.W);
Log($"Material '{mat.Name}' Specular Color (Ks): {mesh.SpecularColor}");
}
else
{
Log($"Material '{mat.Name}' has no Specular Color (Ks), using default.");
}
// Shininess (Ns)
if (mat.HasShininess)
{
mesh.Shininess = mat.Shininess;
Log($"Material '{mat.Name}' Shininess (Ns): {mesh.Shininess}");
}
else
{
Log($"Material '{mat.Name}' has no Shininess (Ns), using default.");
}
// Load textures from files
string? diffusePath = TryResolveTexturePath(mat, TextureType.Diffuse, baseDir);
if (diffusePath != null)
{
mesh.DiffuseTexturePath = diffusePath;
if (loadTextures)
{
try
{
Texture diffuseTexture = TextureManager.LoadTextureFromFile(diffusePath);
mesh.DiffuseTextureHandle = diffuseTexture.Handle;
}
catch (Exception ex)
{
Log($"Error loading diffuse texture '{diffusePath}': {ex.Message}");
mesh.DiffuseTextureHandle = 0;
}
}
}
string? specularPath = TryResolveTexturePath(mat, TextureType.Specular, baseDir);
if (specularPath != null)
{
mesh.SpecularTexturePath = specularPath;
if (loadTextures) {
try
{
Texture specularTexture = TextureManager.LoadTextureFromFile(specularPath);
mesh.SpecularTextureHandle = specularTexture.Handle;
}
catch (Exception ex)
{
Log($"Error loading specular texture '{specularPath}': {ex.Message}");
mesh.SpecularTextureHandle = 0;
}
}
}
result.Add(mesh);
}
}
return new(objPath, result);
}
private static OpenTK.Mathematics.Vector3 ToVec3(System.Numerics.Vector3 v) => new OpenTK.Mathematics.Vector3(v.X, v.Y, v.Z);
private static string? TryResolveTexturePath(Material mat, TextureType type, string baseDir, bool logging = false, bool logToDebug = false)
{
Log($"--- Resolving texture for type: {type} ---", logging, logToDebug);
if (mat.GetMaterialTextureCount(type) <= 0)
{
Log($"No textures of type {type} found in material.", logging, logToDebug);
return null;
}
if (mat.GetMaterialTexture(type, 0, out TextureSlot slot))
{
var raw = slot.FilePath;
Log($"Raw texture path from material: '{raw}'", logging, logToDebug);
if (string.IsNullOrWhiteSpace(raw))
{
Log($"Raw texture path is null or whitespace.", logging, logToDebug);
return null;
}
if (raw.StartsWith("*"))
{
Log($"Raw texture path starts with an asterisk (Assimp internal reference).", logging, logToDebug);
return null; // Assimp internal reference, not a file path
}
var candidate1 = Path.IsPathRooted(raw) ? raw : Path.Combine(baseDir, raw);
Log($"Candidate 1 path: '{candidate1}'", logging, logToDebug);
if (File.Exists(candidate1))
{
Log($"Found texture at candidate 1: '{candidate1}'", logging, logToDebug);
return candidate1;
}
Log($"Candidate 1 not found.", logging, logToDebug);
var normalized = raw.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
var candidate2 = Path.Combine(baseDir, normalized);
Log($"Candidate 2 path (normalized): '{candidate2}'", logging, logToDebug);
if (File.Exists(candidate2))
{
Log($"Found texture at candidate 2: '{candidate2}'", logging, logToDebug);
return candidate2;
}
Log($"Candidate 2 not found.", logging, logToDebug);
var fileName = Path.GetFileName(raw);
Log($"Searching for file name '{fileName}' in '{baseDir}' and subdirectories.", logging, logToDebug);
try
{
foreach (var f in Directory.EnumerateFiles(baseDir, "*.*", SearchOption.AllDirectories))
{
if (string.Equals(Path.GetFileName(f), fileName, StringComparison.OrdinalIgnoreCase))
{
Log($"Found texture by filename search: '{f}'", logging, logToDebug);
return f;
}
}
}
catch (Exception ex)
{
Log($"Error during directory enumeration for texture: {ex.Message}", logging, logToDebug);
}
Log($"File name '{fileName}' not found in '{baseDir}' or subdirectories.", logging, logToDebug);
}
else
{
Log($"Failed to get material texture slot for type {type}.", logging, logToDebug);
}
Log($"--- Failed to resolve texture path for type: {type} ---", logging, logToDebug);
return null;
}
void Log(string message)
{
if(Logging && !LogToDebug) Console.WriteLine(message);
else if(Logging && LogToDebug) Debug.WriteLine(message);
}
static void Log(string message, bool logging, bool logToDebug)
{
if (logging && !logToDebug) Console.WriteLine(message);
else if (logging && logToDebug) Debug.WriteLine(message);
}
}
}
@@ -0,0 +1,108 @@
using OpenTK.Graphics.OpenGL4;
using StbImageSharp; // NEU: Import für StbImageSharp
using System;
using System.Diagnostics;
using System.IO;
namespace WpfOpenGLSubwindowingClasses.ModelHandling
{
public class Texture : IDisposable
{
public int Handle { get; private set; }
public string? Path { get; private set; }
public Texture(int glHandle, string? path)
{
Handle = glHandle;
Path = path;
}
public static Texture LoadFromFile(string path, bool logging = false, bool logToDebug = false)
{
if (!File.Exists(path))
{
Log($"Error: Texture file not found at '{path}'", logging, logToDebug);
return new Texture(0, path);
}
int handle = 0;
try
{
handle = GL.GenTexture();
}
catch(Exception e)
{
#pragma warning disable CS0618 // Type or element is obsolete
if (e is ExecutionEngineException)
{
Log("This is exception should not be thrown by runtime anymore -> unexpected!", logging, logToDebug);
}
#pragma warning restore CS0618
}
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, handle);
StbImage.stbi_set_flip_vertically_on_load(1);
ImageResult? image = null;
try
{
using var fs = File.OpenRead(path);
image = ImageResult.FromStream(fs, ColorComponents.RedGreenBlueAlpha);
}
catch (Exception ex)
{
Log($"Error loading image with StbImageSharp from '{path}': {ex.Message}", logging, logToDebug);
GL.DeleteTexture(handle);
return new Texture(0, path);
}
finally
{
StbImage.stbi_set_flip_vertically_on_load(0);
}
if (image == null)
{
Log($"Error: StbImageSharp returned null for '{path}'", logging, logToDebug);
GL.DeleteTexture(handle);
return new Texture(0, path);
}
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, image.Width, image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
return new Texture(handle, path);
}
public void Use(TextureUnit unit)
{
GL.ActiveTexture(unit);
GL.BindTexture(TextureTarget.Texture2D, Handle);
}
public void Dispose()
{
if (Handle != 0)
{
GL.DeleteTexture(Handle);
Handle = 0;
}
}
static void Log(string message, bool logging, bool logToDebug)
{
if (logging && !logToDebug) Console.WriteLine(message);
else if (logging && logToDebug) Debug.WriteLine(message);
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfOpenGLSubwindowingClasses.ModelHandling
{
public static class TextureManager
{
static private List<Texture> _textures = new();
public static IReadOnlyList<Texture> Textures => _textures;
public static Texture LoadTextureFromFile(string path)
{
var tex = Textures.FirstOrDefault(x => x.Path == path);
if (tex is not null) return tex;
tex = Texture.LoadFromFile(path);
AddTexture(tex);
return tex;
}
public static void AddTexture(Texture texture)
{
if(_textures.Any(x => x.Path == texture.Path)) return;
if (!_textures.Contains(texture)) _textures.Add(texture);
}
public static void RemoveTexture(Texture texture) => _textures.Remove(texture);
public static void RemoveTexture(string path)
{
var tex = _textures.FirstOrDefault(x => x.Path == path);
if (tex is null) return;
_textures.Remove(tex);
}
}
}
@@ -0,0 +1,179 @@
using OpenTK;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Graphics.Wgl;
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using WpfOpenGLSubwindowingClasses.Interop;
using WpfOpenGLSubwindowingClasses.Renderer;
using Wgl = WpfOpenGLSubwindowingClasses.Interop.Wgl;
namespace WpfOpenGLSubwindowingClasses.Controls
{
public class OpenGlHwndHost : HwndHost
{
private IntPtr _hwnd;
private IntPtr _hdc;
private IntPtr _hglrc;
private bool _initializedBindings;
private double _dpiX = 1.0, _dpiY = 1.0;
private static IntPtr s_sharedMasterCtx = IntPtr.Zero;
public OpenGlHwndHost() { } // for xaml
public OpenGLInstance Instance
{
get => (OpenGLInstance)GetValue(InstanceProperty);
set => SetValue(InstanceProperty, value);
}
public static readonly DependencyProperty InstanceProperty =
DependencyProperty.Register(nameof(Instance), typeof(OpenGLInstance),
typeof(OpenGlHwndHost),
new PropertyMetadata(null, OnInstanceChanged));
public bool ShareResourcesWithFirst
{
get => (bool)GetValue(ShareResourcesWithFirstProperty);
set => SetValue(ShareResourcesWithFirstProperty, value);
}
public static readonly DependencyProperty ShareResourcesWithFirstProperty =
DependencyProperty.Register(nameof(ShareResourcesWithFirst), typeof(bool),
typeof(OpenGlHwndHost), new PropertyMetadata(true));
public bool DebugContext { get; set; } = false;
const int WS_CHILD = 0x40000000;
const int WS_VISIBLE = 0x10000000;
public OpenGlHwndHost(OpenGLInstance instance) : this()
{
Instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
var dpi = VisualTreeHelper.GetDpi(Application.Current.MainWindow);
_dpiX = dpi.DpiScaleX;
_dpiY = dpi.DpiScaleY;
_hwnd = Wgl.CreateWindowEx(0, "STATIC", "", WS_CHILD | WS_VISIBLE,
0, 0, 1, 1,
hwndParent.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
_hdc = Wgl.GetDC(_hwnd);
if (_hdc == IntPtr.Zero) throw new InvalidOperationException("GetDC failed.");
if (!Wgl.ApplyDefaultPixelFormat(_hdc))
throw new InvalidOperationException("SetPixelFormat failed.");
IntPtr share = (ShareResourcesWithFirst && s_sharedMasterCtx != IntPtr.Zero) ? s_sharedMasterCtx : IntPtr.Zero;
_hglrc = Wgl.CreateBestContext(_hdc, shareWith: share, debug: DebugContext, coreProfile: true);
if (_hglrc == IntPtr.Zero)
throw new InvalidOperationException("wglCreateContext/CreateBestContext failed.");
if (s_sharedMasterCtx == IntPtr.Zero && ShareResourcesWithFirst)
s_sharedMasterCtx = _hglrc;
Wgl.wglMakeCurrent(_hdc, _hglrc);
if (!_initializedBindings)
{
GL.LoadBindings(new WglBindingsContext());
_initializedBindings = true;
}
var size = GetPixelSize();
InstanceResize(size.width, size.height);
CompositionTarget.Rendering += OnRendering;
return new HandleRef(this, _hwnd);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
CompositionTarget.Rendering -= OnRendering;
Wgl.wglMakeCurrent(IntPtr.Zero, IntPtr.Zero);
if (_hglrc != IntPtr.Zero)
{
Wgl.wglDeleteContext(_hglrc);
_hglrc = IntPtr.Zero;
}
if (_hdc != IntPtr.Zero)
{
Wgl.ReleaseDC(_hwnd, _hdc);
_hdc = IntPtr.Zero;
}
if (hwnd.Handle != IntPtr.Zero)
{
Wgl.DestroyWindow(hwnd.Handle);
}
try
{
Instance?.Dispose();
}
catch { }
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
var size = GetPixelSize();
InstanceResize(size.width, size.height);
}
private void OnRendering(object sender, EventArgs e)
{
if (_hdc == IntPtr.Zero || _hglrc == IntPtr.Zero) return;
Wgl.wglMakeCurrent(_hdc, _hglrc);
Instance.Render();
Wgl.SwapBuffers(_hdc);
Wgl.wglMakeCurrent(IntPtr.Zero, IntPtr.Zero);
}
private (int width, int height) GetPixelSize()
{
int w = (int)Math.Max(1, Math.Round(ActualWidth * _dpiX));
int h = (int)Math.Max(1, Math.Round(ActualHeight * _dpiY));
return (w, h);
}
private void InstanceResize(int pixelWidth, int pixelHeight)
{
try
{
Instance.EnqueueOnGlThread(() =>
{
Instance.Resize(pixelWidth, pixelHeight);
});
}
catch { /* ignore */ }
}
private static void OnInstanceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var host = (OpenGlHwndHost)d;
if (host._hwnd != IntPtr.Zero && host.Instance != null)
{
var size = host.GetPixelSize();
host.InstanceResize(size.width, size.height);
}
}
public IntPtr Hwnd => _hwnd;
public IntPtr Hglrc => _hglrc;
}
}
@@ -0,0 +1,282 @@
using Assimp;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
using WpfOpenGLSubwindowingClasses.Camera;
using WpfOpenGLSubwindowingClasses.ModelHandling;
using WpfOpenGLSubwindowingClasses.Shader;
using PrimitiveType = OpenTK.Graphics.OpenGL4.PrimitiveType;
using Quaternion = OpenTK.Mathematics.Quaternion;
namespace WpfOpenGLSubwindowingClasses.Renderer
{
public class OpenGLInstance(string modelVertexShaderPath, string modelFragmentShaderPath, string axesVertexShaderPath, string axesFragmentShaderPath)
{
private bool _initialized;
private bool _disposeRequested;
private string _modelVertexShaderPath = modelVertexShaderPath;
private string _modelFragmentShaderPath = modelFragmentShaderPath;
private string _axesVertexShaderPath = axesVertexShaderPath;
private string _axesFragmentShaderPath = axesFragmentShaderPath;
private Shader.Shader? _shader;
private Shader.Shader? _colorShader;
private AxisLines _axisLines = new();
public bool DisplayAxisLines { get; set; } = false;
PrimitiveType? DrawModeOverride = null;
List<WpfOpenGLSubwindowingClasses.ModelHandling.Model> Models = new();
public CancellationToken CancellationToken { get; set; }
public Color4 BackgroundColor { get; set; } = Color4.White;
readonly ConcurrentQueue<Action> _glQueue = new();
private void Initialize()
{
if (_initialized) return;
GL.ClearColor(.0f, .0f, .0f, .0f);
GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.Multisample);
GL.Disable(EnableCap.CullFace);
// Shader
_shader = new Shader.Shader(_modelVertexShaderPath, _modelFragmentShaderPath);
_shader.Use();
_colorShader = new Shader.Shader(_axesVertexShaderPath, _axesFragmentShaderPath);
_axisLines.Create(5f);
OrbitCamera.Initialize(new Vector3(-5, -3, 5), Vector3.UnitY);
_initialized = true;
}
public void EnqueueOnGlThread(Action a) => _glQueue.Enqueue(a);
public void Render()
{
if (_disposeRequested || CancellationToken.IsCancellationRequested)
{
Dispose();
return;
}
if (!_initialized) Initialize();
if (_shader == null ||_colorShader == null) return;
while (_glQueue.TryDequeue(out var a)) a();
_shader.Use();
GL.Viewport(0, 0, OrbitCamera.Width, OrbitCamera.Height);
_shader.SetMatrix4("view", OrbitCamera.View);
_shader.SetMatrix4("proj", OrbitCamera.Projection);
_shader.SetVector3("camPos", OrbitCamera.Position);
_shader.SetVector4("lightColor", new Vector4(1.0f, 1.0f, 1.0f, 1.0f));
var lightPos = OrbitCamera.Position - OrbitCamera.Front * 20f;
_shader.SetVector3("lightPos", lightPos);
GL.ClearColor(BackgroundColor);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
foreach(var model in Models) model.Draw(_shader, DrawModeOverride);
if (DisplayAxisLines) _axisLines.Draw(_colorShader, OrbitCamera.View, OrbitCamera.Projection, Matrix4.Identity, 1f);
}
public void SetPolygonMode(PrimitiveType type) => DrawModeOverride = type;
public void Close()
{
_disposeRequested = true;
}
public void Dispose()
{
if (!_initialized) return;
GL.BindVertexArray(0);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
_shader?.Dispose();
_shader = null;
_colorShader?.Dispose();
_colorShader = null;
_axisLines?.Dispose();
foreach (var model in Models) model.Dispose();
Models.Clear();
_initialized = false;
}
public void Resize(int width, int height)
{
OrbitCamera.Width = width;
OrbitCamera.Height = height;
float aspect = (width > 0 && height > 0) ? (float)width / height : 1f;
float fovY = MathHelper.DegreesToRadians(OrbitCamera.Zoom);
OrbitCamera.UpdateProjection(fovY, aspect);
}
public void LoadModel(string path, bool replaceWhenLoaded = false)
{
if (Models.Any(x => x.Path == path)) return;
var loader = new ModelLoader();
WpfOpenGLSubwindowingClasses.ModelHandling.Model? model = null;
try
{
model = loader.Load(path);
}
catch { return; }
if (model is null) return;
model.CenterModelAroundVec();
model.ComputeCenterOffset();
model.ApplyUniformScale(ComputeScaleToTargetRadius(model, 2f));
model.Rotation = Quaternion.FromAxisAngle(Vector3.UnitX, MathHelper.DegreesToRadians(-90f));
if (replaceWhenLoaded) ClearModels();
Models.Add(model);
EnqueueOnGlThread(() =>
{
foreach (var m in model.Meshes) m.CreateGpuBuffers();
});
FocusCameraToModel(model);
}
public async void LoadModelAsync(string path, bool replaceWhenLoaded = false)
{
if (Models.Any(x => x.Path == path)) return;
var loader = new ModelLoader();
WpfOpenGLSubwindowingClasses.ModelHandling.Model? model;
try
{
model = await Task.Run(() => loader.Load(path, loadTextures: false)); // load textures later on ui thread
}
catch { return; }
if (model is null) return;
model.CenterModelAroundVec();
model.ComputeCenterOffset();
model.ApplyUniformScale(ComputeScaleToTargetRadius(model, 2f));
model.Rotation = Quaternion.FromAxisAngle(Vector3.UnitX, MathHelper.DegreesToRadians(-90f));
if (replaceWhenLoaded) ClearModels();
Models.Add(model);
EnqueueOnGlThread(() =>
{
foreach (var m in model.Meshes)
{
m.CreateGpuBuffers();
// Load images on UI Thread so GL is not throwing errors
if (!string.IsNullOrEmpty(m.DiffuseTexturePath) && m.DiffuseTextureHandle == 0)
{
try
{
var diffuseTexture = Texture.LoadFromFile(m.DiffuseTexturePath);
m.DiffuseTextureHandle = diffuseTexture.Handle;
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading diffuse texture '{m.DiffuseTexturePath}': {ex.Message}");
m.DiffuseTextureHandle = 0;
}
}
if (!string.IsNullOrEmpty(m.SpecularTexturePath) && m.SpecularTextureHandle == 0)
{
try
{
var specularTexture = TextureManager.LoadTextureFromFile(m.SpecularTexturePath);
m.SpecularTextureHandle = specularTexture.Handle;
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading specular texture '{m.SpecularTexturePath}': {ex.Message}");
m.SpecularTextureHandle = 0;
}
}
}
});
FocusCameraToModel(model);
}
public void ClearModels()
{
foreach(var model in Models) model.Dispose();
Models.Clear();
}
static float ComputeScaleToTargetRadius(WpfOpenGLSubwindowingClasses.ModelHandling.Model model, float targetRadius)
{
var (min, max) = model.ComputeGlobalBounds();
var center = (min + max) * 0.5f;
float r = 0f;
foreach (var m in model.Meshes)
foreach (var v in m.Vertices)
r = Math.Max(r, (v.Position - center).Length);
return r > 0f ? targetRadius / r : 1f;
}
public void FocusCameraToModel(WpfOpenGLSubwindowingClasses.ModelHandling.Model model)
{
var (min, max) = model.ComputeGlobalBounds();
var center = (min + max) * 0.5f;
float r = 0f;
foreach (var m in model.Meshes)
foreach (var v in m.Vertices)
r = Math.Max(r, (v.Position - center).Length);
OrbitCamera.Target = center;
float aspect = (OrbitCamera.Width > 0 && OrbitCamera.Height > 0) ? (float)OrbitCamera.Width / OrbitCamera.Height : 1f;
float fovY = MathHelper.DegreesToRadians(OrbitCamera.Zoom);
float fovX = 2f * MathF.Atan(MathF.Tan(fovY / 2f) * aspect);
float dY = r / MathF.Tan(fovY / 2f);
float dX = r / MathF.Tan(fovX / 2f);
float dist = MathF.Max(dX, dY) * 1.2f;
OrbitCamera.MinOrbitRadius = r * 1.05f;
OrbitCamera.MaxOrbitRadius = r * 50f;
OrbitCamera.OrbitRadius = Math.Clamp(dist, OrbitCamera.MinOrbitRadius, OrbitCamera.MaxOrbitRadius);
/* no override
float near = Math.Max(0.05f, OrbitCamera.OrbitRadius - 2f * r);
float far = OrbitCamera.OrbitRadius + 4f * r;
near = Math.Max(0.01f, Math.Min(near, far * 0.5f));
OrbitCamera.Near = near;
OrbitCamera.Far = far;
*/
OrbitCamera.RecalculateCameraPositionAndVectors();
OrbitCamera.UpdateProjection(fovY, aspect);
}
}
}
@@ -0,0 +1,140 @@
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using System.Diagnostics;
using System.IO;
namespace WpfOpenGLSubwindowingClasses.Shader
{
public class Shader : IDisposable
{
public int Program;
private bool disposedValue = false;
public bool Logging { get; set; } = false;
public bool LogToDebug { get; set; } = false;
public Shader(string vertexPath, string fragmentPath, bool logging = false, bool logToDebug = false)
{
Logging = logging;
LogToDebug = logToDebug;
string VertexShaderSource = File.ReadAllText(vertexPath);
string FragmentShaderSource = File.ReadAllText(fragmentPath);
(int VertexShader, int FragmentShader) = GenerateAndBindShaders(VertexShaderSource, FragmentShaderSource);
if (CompileShaders(VertexShader, FragmentShader) == 0) throw new Exception("Failed to compile shaders");
if (LinkShadersToProgram(VertexShader, FragmentShader) == 0) throw new Exception("Failed to link Shaders to Program");
Cleanup(VertexShader, FragmentShader);
}
public void SetMatrix4(string name, Matrix4 value) => GL.UniformMatrix4(GL.GetUniformLocation(Program, name), false, ref value);
public void SetVector3(string name, Vector3 value) => GL.Uniform3(GL.GetUniformLocation(Program, name), value);
public void SetVector4(string name, Vector4 value) => GL.Uniform4(GL.GetUniformLocation(Program, name), value);
public void SetColor4(string name, Color4 value) => GL.Uniform4(GL.GetUniformLocation(Program, name), value);
public void SetFloat(string name, float value) => GL.Uniform1(GL.GetUniformLocation(Program, name), value);
public void SetInt(string name, int value) => GL.Uniform1(GL.GetUniformLocation(Program, name), value);
public void SetBool(string name, bool value) => GL.Uniform1(GL.GetUniformLocation(Program, name), value ? 1 : 0);
private (int, int) GenerateAndBindShaders(string VertexShaderSource, string FragmentShaderSource)
{
int VertexShader, FragmentShader;
// Generate and Bind
VertexShader = GL.CreateShader(ShaderType.VertexShader);
GL.ShaderSource(VertexShader, VertexShaderSource);
FragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(FragmentShader, FragmentShaderSource);
return (VertexShader, FragmentShader);
}
private int CompileShaders(int VertexShader, int FragmentShader)
{
// Compile
GL.CompileShader(VertexShader);
GL.GetShader(VertexShader, ShaderParameter.CompileStatus, out int success1);
if (success1 == 0)
{
string infoLog = GL.GetShaderInfoLog(VertexShader);
Log(infoLog);
return 0;
}
GL.CompileShader(FragmentShader);
GL.GetShader(FragmentShader, ShaderParameter.CompileStatus, out int success2);
if (success2 == 0)
{
string infoLog = GL.GetShaderInfoLog(FragmentShader);
Log(infoLog);
return 0;
}
return 1;
}
private int LinkShadersToProgram(int VertexShader, int FragmentShader)
{
Program = GL.CreateProgram();
GL.AttachShader(Program, VertexShader);
GL.AttachShader(Program, FragmentShader);
GL.LinkProgram(Program);
GL.GetProgram(Program, GetProgramParameterName.LinkStatus, out int success);
if (success == 0)
{
string infoLog = GL.GetProgramInfoLog(Program);
Log(infoLog);
return 0;
}
return 1;
}
private void Cleanup(int VertexShader, int FragmentShader)
{
GL.DetachShader(Program, VertexShader);
GL.DetachShader(Program, FragmentShader);
GL.DeleteShader(FragmentShader);
GL.DeleteShader(VertexShader);
}
public void Use() => GL.UseProgram(Program);
public virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
GL.DeleteProgram(Program);
disposedValue = true;
}
}
~Shader()
{
if (disposedValue == false)
{
Log("GPU Resource leak! Did you forget to call Dispose()?");
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
void Log(string message)
{
if (Logging && !LogToDebug) Console.WriteLine(message);
else if (Logging && LogToDebug) Debug.WriteLine(message);
}
}
}
@@ -0,0 +1,95 @@
#version 410 core
in DATA {
vec3 Normal;
vec3 color;
vec2 texCoord;
vec3 WorldPos;
} data_in;
vec2 texCoord = data_in.texCoord;
vec3 Normal = data_in.Normal;
vec3 crntPos = data_in.WorldPos;
layout(location = 0) out vec4 outColor;
uniform sampler2D diffuse0;
uniform sampler2D specular0;
uniform vec4 material_ambient;
uniform vec4 material_diffuse;
uniform vec4 material_specular;
uniform float material_shininess;
uniform bool hasDiffuseTexture;
uniform bool hasSpecularTexture;
uniform vec4 lightColor;
uniform vec3 lightPos;
uniform vec3 camPos;
vec4 directLight()
{
vec3 final_ambient_color;
vec3 final_diffuse_color;
vec3 final_specular_color;
if(hasDiffuseTexture){
final_diffuse_color = texture(diffuse0, texCoord).rgb;
final_ambient_color = material_ambient.rgb;
}
else{
final_ambient_color = material_ambient.rgb;
final_diffuse_color = material_diffuse.rgb;
}
if(hasSpecularTexture){
final_specular_color = texture(specular0, texCoord).rgb;
}
else {
final_specular_color = material_specular.rgb;
}
// ambient lighting
float ambientStrength = 0.20f;
vec3 ambient = ambientStrength * final_ambient_color;
// diffuse lighting
vec3 normal = normalize(Normal);
vec3 lightDirection = normalize(lightPos - crntPos);
float diff = max(dot(normal, lightDirection), 0.0f);
vec3 diffuse = diff * final_diffuse_color;
// specular lighting
vec3 specular = vec3(0.0f);
if(diff > 0.0f){
float specularStrength = 0.50f;
vec3 viewDirection = normalize(camPos - crntPos);
vec3 reflectionDirection = reflect(-lightDirection, normal);
// Blinn-Phong Model (Halway Vector)
vec3 halfwayDir = normalize(lightDirection + viewDirection);
float spec = pow(max(dot(normal, halfwayDir), 0.0f), material_shininess);
specular = specularStrength * spec * final_specular_color;
}
return vec4(ambient + diffuse + specular, 1.0f) * lightColor;
}
vec4 noLight()
{
vec3 final_color;
if(hasDiffuseTexture){
final_color = texture(diffuse0, texCoord).rgb;
}
else{
final_color = material_diffuse.rgb;
}
return vec4(final_color, 1.0f); // Rückgabe der Farbe mit voller Deckkraft
}
void main(){
outColor = directLight();
}
@@ -0,0 +1,31 @@
#version 410 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec3 aColor;
layout (location = 3) in vec2 aTexCoord;
out DATA {
vec3 Normal;
vec3 color;
vec2 texCoord;
vec3 WorldPos;
} data_out;
uniform mat4 model;
// uniform mat4 camMatrix;
uniform mat4 view;
uniform mat4 proj;
uniform mat4 translation;
uniform mat4 rotation;
uniform mat4 scale;
void main() {
mat4 M = translation * rotation * scale * model;
vec4 worldPos = M * vec4(aPos, 1.0);
gl_Position = proj * view * worldPos;
data_out.WorldPos = worldPos.xyz;
data_out.Normal = mat3(transpose(inverse(M))) * aNormal;
data_out.color = aColor;
data_out.texCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}
@@ -0,0 +1,9 @@
#version 410 core
in vec3 vColor;
out vec4 FragColor;
void main(){
FragColor = vec4(vColor, 1.0);
}
@@ -0,0 +1,15 @@
#version 410 core
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
uniform mat4 view;
uniform mat4 proj;
uniform mat4 model;
out vec3 vColor;
void main(){
vColor = aColor;
gl_Position = proj * view * model * vec4(aPos,1.0);
}
@@ -0,0 +1,241 @@
using System;
using System.Runtime.InteropServices;
namespace WpfOpenGLSubwindowingClasses.Interop
{
internal static class Wgl
{
// PIXELFORMATDESCRIPTOR
[StructLayout(LayoutKind.Sequential)]
public struct PIXELFORMATDESCRIPTOR
{
public ushort nSize;
public ushort nVersion;
public uint dwFlags;
public byte iPixelType;
public byte cColorBits;
public byte cRedBits;
public byte cRedShift;
public byte cGreenBits;
public byte cGreenShift;
public byte cBlueBits;
public byte cBlueShift;
public byte cAlphaBits;
public byte cAlphaShift;
public byte cAccumBits;
public byte cAccumRedBits;
public byte cAccumGreenBits;
public byte cAccumBlueBits;
public byte cAccumAlphaBits;
public byte cDepthBits;
public byte cStencilBits;
public byte cAuxBuffers;
public byte iLayerType;
public byte bReserved;
public uint dwLayerMask;
public uint dwVisibleMask;
public uint dwDamageMask;
}
// PFD Flags
public const uint PFD_DRAW_TO_WINDOW = 0x00000004;
public const uint PFD_SUPPORT_OPENGL = 0x00000020;
public const uint PFD_DOUBLEBUFFER = 0x00000001;
public const byte PFD_TYPE_RGBA = 0;
public const byte PFD_MAIN_PLANE = 0;
// Window Styles
public const int WS_CHILD = 0x40000000;
public const int WS_VISIBLE = 0x10000000;
public const int WS_CLIPSIBLINGS = 0x04000000;
public const int WS_CLIPCHILDREN = 0x02000000;
// WGL_ARB_create_context constants
public const int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
public const int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092;
public const int WGL_CONTEXT_FLAGS_ARB = 0x2094;
public const int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126;
public const int WGL_CONTEXT_CORE_PROFILE_BIT_ARB = 0x0001;
public const int WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x0002;
public const int WGL_CONTEXT_DEBUG_BIT_ARB = 0x0001;
public const int WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
// Win32/GDI
[DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")] public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")] public static extern int ChoosePixelFormat(IntPtr hdc, ref PIXELFORMATDESCRIPTOR pfd);
[DllImport("gdi32.dll")] public static extern bool SetPixelFormat(IntPtr hdc, int format, ref PIXELFORMATDESCRIPTOR pfd);
[DllImport("gdi32.dll")] public static extern bool SwapBuffers(IntPtr hdc);
// WGL core
[DllImport("opengl32.dll")] public static extern IntPtr wglCreateContext(IntPtr hdc);
[DllImport("opengl32.dll")] public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr hglrc);
[DllImport("opengl32.dll")] public static extern bool wglDeleteContext(IntPtr hglrc);
[DllImport("opengl32.dll")] public static extern bool wglShareLists(IntPtr hglrcSrc, IntPtr hglrcDst);
[DllImport("opengl32.dll")] public static extern IntPtr wglGetProcAddress(string name);
// Win32 window creation
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr CreateWindowEx(
int exStyle,
string className,
string windowName,
int style,
int x, int y, int width, int height,
IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param);
[DllImport("user32.dll")]
public static extern bool DestroyWindow(IntPtr hwnd);
// Kernel32 for function address fallback
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr LoadLibrary(string lpFileName);
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
// Convenience: Make/Done Current
public static bool MakeCurrent(IntPtr hdc, IntPtr hglrc) => wglMakeCurrent(hdc, hglrc);
public static void DoneCurrent() => wglMakeCurrent(IntPtr.Zero, IntPtr.Zero);
public static bool DeleteContext(IntPtr hglrc) => wglDeleteContext(hglrc);
// Set a reasonable default PixelFormat (RGBA, double-buffer, depth/stencil)
public static bool ApplyDefaultPixelFormat(IntPtr hdc, int colorBits = 24, int depthBits = 24, int stencilBits = 8)
{
var pfd = new PIXELFORMATDESCRIPTOR
{
nSize = (ushort)Marshal.SizeOf<PIXELFORMATDESCRIPTOR>(),
nVersion = 1,
dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
iPixelType = PFD_TYPE_RGBA,
cColorBits = (byte)colorBits,
cDepthBits = (byte)depthBits,
cStencilBits = (byte)stencilBits,
iLayerType = PFD_MAIN_PLANE
};
int format = ChoosePixelFormat(hdc, ref pfd);
if (format == 0) return false;
return SetPixelFormat(hdc, format, ref pfd);
}
// Delegates for WGL extensions
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr wglCreateContextAttribsARB_t(IntPtr hdc, IntPtr hShareContext, int[] attribList);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate bool wglSwapIntervalEXT_t(int interval);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int wglGetSwapIntervalEXT_t();
private static wglSwapIntervalEXT_t? s_swapIntervalEXT;
private static wglGetSwapIntervalEXT_t? s_getSwapIntervalEXT;
private static void EnsureSwapIntervalDelegates()
{
if (s_swapIntervalEXT == null)
{
var p = wglGetProcAddress("wglSwapIntervalEXT");
if (p != IntPtr.Zero)
s_swapIntervalEXT = (wglSwapIntervalEXT_t)Marshal.GetDelegateForFunctionPointer(p, typeof(wglSwapIntervalEXT_t));
}
if (s_getSwapIntervalEXT == null)
{
var p = wglGetProcAddress("wglGetSwapIntervalEXT");
if (p != IntPtr.Zero)
s_getSwapIntervalEXT = (wglGetSwapIntervalEXT_t)Marshal.GetDelegateForFunctionPointer(p, typeof(wglGetSwapIntervalEXT_t));
}
}
// Try to set VSync (interval 0=off, 1=on)
public static bool TrySetSwapInterval(int interval)
{
EnsureSwapIntervalDelegates();
return s_swapIntervalEXT != null && s_swapIntervalEXT(interval);
}
// Get current swap interval (-1 if unavailable)
public static int GetSwapInterval()
{
EnsureSwapIntervalDelegates();
return s_getSwapIntervalEXT != null ? s_getSwapIntervalEXT() : -1;
}
// Create a "best" OpenGL context:
// - tries wglCreateContextAttribsARB for core/modern versions
// - falls back to legacy if needed
// - optionally shares resources with 'shareWith' via wglShareLists
public static IntPtr CreateBestContext(IntPtr hdc, IntPtr shareWith = default, bool debug = false, bool coreProfile = true)
{
// Create temp legacy context to load the ARB function (requires current context)
IntPtr temp = wglCreateContext(hdc);
if (temp != IntPtr.Zero) wglMakeCurrent(hdc, temp);
IntPtr proc = wglGetProcAddress("wglCreateContextAttribsARB");
IntPtr realCtx = IntPtr.Zero;
if (proc != IntPtr.Zero)
{
var createAttribs = (wglCreateContextAttribsARB_t)Marshal.GetDelegateForFunctionPointer(proc, typeof(wglCreateContextAttribsARB_t));
int flags = 0;
if (debug) flags |= WGL_CONTEXT_DEBUG_BIT_ARB;
if (coreProfile) flags |= WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB;
// Try descending versions
int[][] versions =
{
new[]{4,6}, new[]{4,5}, new[]{4,4}, new[]{4,3}, new[]{4,2}, new[]{4,1}, new[]{4,0},
new[]{3,3}, new[]{3,2}
};
foreach (var v in versions)
{
int[] attribs =
{
WGL_CONTEXT_MAJOR_VERSION_ARB, v[0],
WGL_CONTEXT_MINOR_VERSION_ARB, v[1],
WGL_CONTEXT_FLAGS_ARB, flags,
WGL_CONTEXT_PROFILE_MASK_ARB, coreProfile ? WGL_CONTEXT_CORE_PROFILE_BIT_ARB : WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
0
};
realCtx = createAttribs(hdc, shareWith, attribs);
if (realCtx != IntPtr.Zero) break;
}
}
// Fallback to legacy
if (realCtx == IntPtr.Zero)
{
realCtx = wglCreateContext(hdc);
if (realCtx == IntPtr.Zero) return IntPtr.Zero;
if (shareWith != IntPtr.Zero)
wglShareLists(shareWith, realCtx);
}
// Cleanup temp
if (temp != IntPtr.Zero)
{
wglMakeCurrent(IntPtr.Zero, IntPtr.Zero);
wglDeleteContext(temp);
}
return realCtx;
}
// Helper to create a child window using the built-in "STATIC" class.
// This is suitable as a render target HWND for OpenGL.
public static IntPtr CreateChildWindow(IntPtr parent, int x, int y, int width, int height, bool visible = true)
{
int style = WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | (visible ? WS_VISIBLE : 0);
return CreateWindowEx(
0,
"STATIC",
"",
style,
x, y, width, height,
parent, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}
}
}
@@ -0,0 +1,21 @@
using OpenTK;
using OpenTK.Graphics.Wgl;
using System;
using WpfOpenGLSubwindowingClasses.Interop;
namespace WpfOpenGLSubwindowingClasses.Interop
{
internal sealed class WglBindingsContext : IBindingsContext
{
private readonly IntPtr _opengl32 = Wgl.LoadLibrary("opengl32.dll");
public IntPtr GetProcAddress(string procName)
{
var ptr = Wgl.wglGetProcAddress(procName);
if (ptr == IntPtr.Zero && _opengl32 != IntPtr.Zero)
ptr = Wgl.GetProcAddress(_opengl32, procName);
return ptr;
}
}
}
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Remove="Shaders\BasicShader.frag" />
<None Remove="Shaders\BasicShader.vert" />
<None Remove="Shaders\ColorShader.frag" />
<None Remove="Shaders\ColorShader.vert" />
</ItemGroup>
<ItemGroup>
<Content Include="Shaders\BasicShader.frag">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Shaders\BasicShader.vert">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Shaders\ColorShader.frag">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Shaders\ColorShader.vert">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="AssimpNetter" Version="6.0.2.1" />
<PackageReference Include="OpenTK" Version="4.9.4" />
<PackageReference Include="StbImageSharp" Version="2.30.15" />
</ItemGroup>
</Project>