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
+428
View File
@@ -0,0 +1,428 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
*.env
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Dd]ebug/x64/
[Dd]ebugPublic/x64/
[Rr]elease/x64/
[Rr]eleases/x64/
bin/x64/
obj/x64/
[Dd]ebug/x86/
[Dd]ebugPublic/x86/
[Rr]elease/x86/
[Rr]eleases/x86/
bin/x86/
obj/x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
[Aa][Rr][Mm]64[Ee][Cc]/
bld/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Build results on 'Bin' directories
**/[Bb]in/*
# Uncomment if you have tasks that rely on *.refresh files to move binaries
# (https://github.com/github/gitignore/pull/3736)
#!**/[Bb]in/*.refresh
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Approval Tests result files
*.received.*
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.idb
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
**/.paket/paket.exe
paket-files/
# FAKE - F# Make
**/.fake/
# CodeRush personal settings
**/.cr/personal
# Python Tools for Visual Studio (PTVS)
**/__pycache__/
*.pyc
# Cake - Uncomment if you are using it
#tools/**
#!tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
MSBuild_Logs/
# AWS SAM Build and Temporary Artifacts folder
.aws-sam
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
**/.mfractor/
# Local History for Visual Studio
**/.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
**/.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
+5
View File
@@ -0,0 +1,5 @@
# WpfOpenGLSubwindowing
## Info
This Project aims to share my experience with aOpenGL (OpenTK), AssimpNet and GLWpfControl to load 3D models in C#.
Currently utilizes an Orbit Camera to view Model fixed to Center.
@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11217.181
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfOpenGLSubwindowing", "WpfOpenGLSubwindowing\WpfOpenGLSubwindowing.csproj", "{1E038559-E21A-419A-8C83-EAB9A873B8F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfOpenGLSubwindowingClasses", "..\WpfOpenGLSubwindowingClasses\WpfOpenGLSubwindowingClasses\WpfOpenGLSubwindowingClasses.csproj", "{ABBFC151-B834-02DE-CE06-2CEEB90CD87F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1E038559-E21A-419A-8C83-EAB9A873B8F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E038559-E21A-419A-8C83-EAB9A873B8F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E038559-E21A-419A-8C83-EAB9A873B8F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E038559-E21A-419A-8C83-EAB9A873B8F8}.Release|Any CPU.Build.0 = Release|Any CPU
{ABBFC151-B834-02DE-CE06-2CEEB90CD87F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABBFC151-B834-02DE-CE06-2CEEB90CD87F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABBFC151-B834-02DE-CE06-2CEEB90CD87F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABBFC151-B834-02DE-CE06-2CEEB90CD87F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {20CD5B98-9C10-4C9B-8D20-B0CDC0ED1A34}
EndGlobalSection
EndGlobal
@@ -0,0 +1,9 @@
<Application x:Class="WpfOpenGLSubwindowing.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfOpenGLSubwindowing"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
@@ -0,0 +1,14 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace WpfOpenGLSubwindowing
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
@@ -0,0 +1,80 @@
<Window x:Class="WpfOpenGLSubwindowing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfOpenGLSubwindowing"
xmlns:vm="clr-namespace:WpfOpenGLSubwindowing.ViewModel"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:gl="clr-namespace:OpenTK.Wpf;assembly=GLWpfControl"
mc:Ignorable="d"
Title="{Binding Title}" Height="450" Width="800"
>
<Window.DataContext>
<vm:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="5*"/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" CornerRadius="7.5" BorderBrush="Black" BorderThickness="1" Margin="5">
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Button Command="{Binding LoadModelCommand}" Content="Load Model" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20"/>
<CheckBox Command="{Binding EnableAxisLinesCommand}" Content="Enable Axis Lines" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20"/>
<ListBox SelectedItem="{Binding SelectedDrawType}" ItemsSource="{Binding DrawTypes}" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20"/>
<TextBlock Text="Background Color:" Margin="5,0,0,10"/>
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Margin="5" Padding="5">
<StackPanel>
<WrapPanel HorizontalAlignment="Stretch">
<TextBlock Text="R:" Margin="0,0,10,0"/>
<Slider Value="{Binding ColorR}" Minimum="0.0" Maximum="1.0" Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type WrapPanel}}}"/>
</WrapPanel>
<WrapPanel>
<TextBlock Text="G:" Margin="0,0,10,0"/>
<Slider Value="{Binding ColorG}" Minimum="0.0" Maximum="1.0" Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type WrapPanel}}}"/>
</WrapPanel>
<WrapPanel>
<TextBlock Text="B:" Margin="0,0,10,0"/>
<Slider Value="{Binding ColorB}" Minimum="0.0" Maximum="1.0" Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type WrapPanel}}}"/>
</WrapPanel>
<WrapPanel>
<TextBlock Text="A:" Margin="0,0,10,0"/>
<Slider Value="{Binding ColorA}" Minimum="0.0" Maximum="1.0" Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type WrapPanel}}}"/>
</WrapPanel>
</StackPanel>
</Border>
</StackPanel>
</Border>
<Border Grid.Column="1" CornerRadius="7.5" BorderBrush="Black" BorderThickness="1" Margin="5" Padding="5">
<gl:GLWpfControl x:Name="OpenTkControl"
Render="OpenTkControl_Render"
Loaded="OpenTkControl_Loaded"
MouseDown="OpenTkControl_MouseDown"
MouseMove="OpenTkControl_MouseMove"
MouseUp="OpenTkControl_MouseUp"
MouseWheel="OpenTkControl_MouseWheel"
SizeChanged="OpenTkControl_SizeChanged"
MouseLeave="OpenTkControl_LostFocus"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Unloaded">
<i:InvokeCommandAction Command="{Binding ClosingGLCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</gl:GLWpfControl>
</Border>
<Border Grid.Column="2" CornerRadius="7.5" BorderBrush="Black" BorderThickness="1" Margin="5" Padding="5">
<Grid x:Name="HwndRootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="0*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="HwndFirstGrid"/>
<Grid Grid.Row="1" x:Name="HwndSecondGrid"/>
</Grid>
</Border>
</Grid>
</Window>
@@ -0,0 +1,122 @@
using OpenTK.Mathematics;
using OpenTK.Wpf;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfOpenGLSubwindowing.ViewModel;
using WpfOpenGLSubwindowingClasses.Camera;
using WpfOpenGLSubwindowingClasses.Controls;
using WpfOpenGLSubwindowingClasses.Renderer;
namespace WpfOpenGLSubwindowing
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private OpenGlHwndHost _hostTop;
private OpenGlHwndHost _hostBottom;
private OpenGLInstance _glTop;
public OpenGLInstance SecondaryGLInstance;
public MainWindow()
{
InitializeComponent();
if (DataContext is not MainWindowViewModel vm) return;
vm.MainWindowInstance = this;
Render = vm.OnRender;
LoadedHandler = vm.OnLoadGL;
MouseDownHandler = OrbitCamera.MouseDownEvent;
MouseMoveHandler = OrbitCamera.MouseMoveEvent;
MouseUpHandler = OrbitCamera.MouseUpEvent;
MouseWheelHandler = OrbitCamera.MouseWheelEvent;
MouseLostFocusHandler = OrbitCamera.MouseLostFocusEvent;
var settings = new GLWpfControlSettings
{
MajorVersion = 4,
MinorVersion = 6,
Samples = 16,
};
OpenTkControl.Start(settings);
OpenTkControl.RenderContinuously = true;
// _glTop = new OpenGLInstance(@"Shaders\BasicShader.vert", @"Shaders\BasicShader.frag", @"Shaders\ColorShader.vert", @"Shaders\ColorShader.frag");
SecondaryGLInstance = new OpenGLInstance(@"Shaders\BasicShader.vert", @"Shaders\BasicShader.frag", @"Shaders\ColorShader.vert", @"Shaders\ColorShader.frag");
// _hostTop = new OpenGlHwndHost(_glTop) { ShareResourcesWithFirst = true };
_hostBottom = new OpenGlHwndHost(SecondaryGLInstance) { ShareResourcesWithFirst = true };
// HwndFirstGrid.Children.Add(_hostTop);
HwndSecondGrid.Children.Add(_hostBottom);
// _glTop.LoadModelAsync(@"Models\12140_Skull_v3_L2.obj", replaceWhenLoaded: true);
// SecondaryGLInstance.LoadModelAsync(@"Models\12140_Skull_v3_L2.obj", replaceWhenLoaded: true);
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
// Sauber beenden
_glTop?.Close();
SecondaryGLInstance?.Close();
}
public Action<TimeSpan> Render;
public Action<GLWpfControl> LoadedHandler;
public Action<object, MouseButtonEventArgs> MouseDownHandler;
public Action<object, MouseEventArgs> MouseMoveHandler;
public Action<object, MouseButtonEventArgs> MouseUpHandler;
public Action<object, MouseWheelEventArgs> MouseWheelHandler;
public Action<object, RoutedEventArgs> MouseLostFocusHandler;
public Action<object, SizeChangedEventArgs> SizeChangedHandler;
private void OpenTkControl_Render(TimeSpan obj) => Render(obj);
private void OpenTkControl_Loaded(object sender, RoutedEventArgs e) => LoadedHandler(OpenTkControl);
private void OpenTkControl_MouseDown(object sender, MouseButtonEventArgs e) => MouseDownHandler(sender, e);
private void OpenTkControl_MouseMove(object sender, MouseEventArgs e) => MouseMoveHandler(sender, e);
private void OpenTkControl_MouseUp(object sender, MouseButtonEventArgs e) => MouseUpHandler(sender, e);
private void OpenTkControl_MouseWheel(object sender, MouseWheelEventArgs e) => MouseWheelHandler(sender, e);
private void OpenTkControl_LostFocus(object sender, RoutedEventArgs e) => MouseLostFocusHandler(sender, e);
private void OpenTkControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
var visual = sender as Visual ?? Application.Current.MainWindow;
double dpiX = 1.0, dpiY = 1.0;
var ps = PresentationSource.FromVisual(visual);
if (ps?.CompositionTarget != null)
{
dpiX = ps.CompositionTarget.TransformToDevice.M11;
dpiY = ps.CompositionTarget.TransformToDevice.M22;
}
int pxW = Math.Max(1, (int)Math.Round(e.NewSize.Width * dpiX));
int pxH = Math.Max(1, (int)Math.Round(e.NewSize.Height * dpiY));
OrbitCamera.Width = pxW;
OrbitCamera.Height = pxH;
float aspect = pxH > 0 ? (float)pxW / (float)pxH : 1f;
OrbitCamera.UpdateProjection(MathHelper.DegreesToRadians(OrbitCamera.Zoom), aspect);
}
}
}
@@ -0,0 +1,143 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Wpf;
using System;
using System.Diagnostics;
using System.Numerics;
using System.Windows;
using WpfOpenGLSubwindowingClasses.Camera;
using WpfOpenGLSubwindowingClasses.ModelHandling;
using WpfOpenGLSubwindowingClasses.Renderer;
namespace WpfOpenGLSubwindowing.ViewModel
{
public partial class MainWindowViewModel : ObservableObject
{
public int ViewportWidth { get; set; } = 1280;
public int ViewportHeight { get; set; } = 720;
public string Title { get; set; } = "WpfOpenGLSubwindowing";
public MainWindow? MainWindowInstance { get; set; }
private WpfOpenGLSubwindowingClasses.Renderer.OpenGLInstance? _renderer;
private CancellationTokenSource _rCTS = new();
public float? ColorR
{
get => _renderer?.BackgroundColor.R ?? 1.0f;
set
{
if (_renderer == null || value == null || _renderer.BackgroundColor.R == value) return;
_renderer.BackgroundColor = new OpenTK.Mathematics.Color4((float)value, _renderer.BackgroundColor.G, _renderer.BackgroundColor.B, _renderer.BackgroundColor.A);
OnPropertyChanged(nameof(ColorR));
}
}
public float? ColorG
{
get => _renderer?.BackgroundColor.G ?? 1.0f;
set
{
if (_renderer == null || value == null || _renderer.BackgroundColor.G == value) return;
_renderer.BackgroundColor = new OpenTK.Mathematics.Color4(_renderer.BackgroundColor.R, (float)value, _renderer.BackgroundColor.B, _renderer.BackgroundColor.A);
OnPropertyChanged(nameof(ColorG));
}
}
public float? ColorB
{
get => _renderer?.BackgroundColor.B ?? 1.0f;
set
{
if (_renderer == null || value == null || _renderer.BackgroundColor.B == value) return;
_renderer.BackgroundColor = new OpenTK.Mathematics.Color4(_renderer.BackgroundColor.R, _renderer.BackgroundColor.G, (float)value, _renderer.BackgroundColor.A);
OnPropertyChanged(nameof(ColorB));
}
}
public float? ColorA
{
get => _renderer?.BackgroundColor.A ?? 1.0f;
set
{
if (_renderer == null || value == null || _renderer.BackgroundColor.A == value) return;
_renderer.BackgroundColor = new OpenTK.Mathematics.Color4(_renderer.BackgroundColor.R, _renderer.BackgroundColor.G, _renderer.BackgroundColor.B, (float)value);
OnPropertyChanged(nameof(ColorA));
}
}
GLWpfControl? _view;
private bool _disposeRequested;
public IRelayCommand<GLWpfControl> ClosingGLCommand { get; }
public IRelayCommand LoadModelCommand { get; }
public IRelayCommand EnableAxisLinesCommand { get; }
public PrimitiveType SelectedDrawType
{
get;
set
{
field = value;
if (_renderer is not null) _renderer.SetPolygonMode(SelectedDrawType);
}
} = PrimitiveType.Triangles;
public List<PrimitiveType> DrawTypes { get; } = new()
{
PrimitiveType.Triangles,
PrimitiveType.Lines,
PrimitiveType.Points,
};
public MainWindowViewModel()
{
ClosingGLCommand = new RelayCommand<GLWpfControl>(_ => _disposeRequested = true);
LoadModelCommand = new RelayCommand(LoadModel);
EnableAxisLinesCommand = new RelayCommand(() => { if (_renderer is not null) _renderer.DisplayAxisLines = !_renderer.DisplayAxisLines; });
}
public void OnLoadGL(GLWpfControl? view)
{
if (view == null) return;
_view = view;
_renderer = new OpenGLInstance(@"Shaders\BasicShader.vert", @"Shaders\BasicShader.frag", @"Shaders\ColorShader.vert", @"Shaders\ColorShader.frag");
}
public void OnRender(TimeSpan delta)
{
if (_renderer == null || _view == null)
return;
if (_disposeRequested)
{
_renderer.Dispose();
_renderer = null;
_view.Render -= OnRender;
_view = null;
_disposeRequested = false;
return;
}
_renderer.Render();
}
private void LoadModel()
{
string path = @"Models/12140_Skull_v3_L2.obj";
OpenFileDialog dialog = new();
// dialog.Filter = "obj files (*.obj)|*.obj";
if(dialog.ShowDialog() == true)
{
path = dialog.FileName;
}
_renderer?.LoadModelAsync(path, true);
MainWindowInstance?.SecondaryGLInstance.LoadModelAsync(path, true);
}
}
}
@@ -0,0 +1,16 @@
# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 21.12.2011 14:29:19
newmtl 12140_Skull_v3
Ns 24.0000
Ni 1.5000
d 1.0000
Tr 0.0000
Tf 1.0000 1.0000 1.0000
illum 2
Ka 1.0000 1.0000 1.0000
Kd 1.0000 1.0000 1.0000
Ks 0.1170 0.1170 0.1170
Ke 0.0000 0.0000 0.0000
map_Ka Skull.jpg
map_Kd Skull.jpg
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

@@ -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,59 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Remove="Models\12140_Skull_v3_L2.mtl" />
<None Remove="Models\12140_Skull_v3_L2.obj" />
<None Remove="Models\Skull.jpg" />
<None Remove="Shaders\BasicShader.frag" />
<None Remove="Shaders\BasicShader.vert" />
<None Remove="Shaders\ColorShader.frag" />
<None Remove="Shaders\ColorShader.vert" />
</ItemGroup>
<ItemGroup>
<Content Include="Models\12140_Skull_v3_L2.mtl">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Models\12140_Skull_v3_L2.obj">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Models\Skull.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<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="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="OpenTK" Version="4.9.4" />
<PackageReference Include="OpenTK.GLWpfControl" Version="4.3.3" />
<PackageReference Include="StbImageSharp" Version="2.30.15" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WpfOpenGLSubwindowingClasses\WpfOpenGLSubwindowingClasses\WpfOpenGLSubwindowingClasses.csproj" />
</ItemGroup>
</Project>
@@ -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>