Внимание: всего OpenGL / TK newb, так что будьте добры. Возможно, я откусила больше, чем могу пережевать.
Технические характеристики:
- Визуальная студия Community 2017
- C # .Net 4.6.1 (с использованием WPF, элемента управления WindowsFormsHost)
- OpenTK 3.0.1
- OpenTK.GLControl 3.0.1
Я пытаюсь скопировать учебник Cubemap с веб-сайта learnopengl.com (с которым мне удалось работать). Я больше знаком с C #, поэтому было бы лучше получить решение, работающее с OpenTK. Все, что я получаю, это пустой экран. Возможно, задняя часть изображения (слишком оптимистично?). Любая помощь будет оценена.
Дайте мне знать, если я что-то пропустил.
Вот код: (Я чувствую, что я рядом.)
MainWindow.xaml.cs
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Forms;
using Path = System.IO.Path;
namespace OpenTKTesting
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private int _vertexBufferObject;
private int _vertexArrayObject;
private Shader shader;
// For documentation on this, check Texture.cs
private TextureCubemap cubemap;
private Camera camera;
GLControl glControl; // the winforms opentk control
public MainWindow()
{
InitializeComponent();
glControl = new GLControl(new GraphicsMode(32, 24, 0, 8)) { VSync = true };
windowsFormsHost.Child = glControl;
System.Windows.Forms.Integration.WindowsFormsHost.EnableWindowsFormsInterop();
Toolkit.Init();
}
private List<string> ImageFaces
{
get;
set;
}
public void GetImageFaces()
{
ImageFaces = new List<string>();
string dir = @"C:\\Development\\ScratchDev\\OpenTKTesting\\OpenTKTesting\\Resources";
ImageFaces.Add(Path.Combine(dir, "f.jpg"));
ImageFaces.Add(Path.Combine(dir, "b.jpg"));
ImageFaces.Add(Path.Combine(dir, "u.jpg"));
ImageFaces.Add(Path.Combine(dir, "d.jpg"));
ImageFaces.Add(Path.Combine(dir, "r.jpg"));
ImageFaces.Add(Path.Combine(dir, "l.jpg"));
}
private void SetupGLControl()
{
if (glControl == null)
{
return;
}
GetImageFaces();
foreach (var item in ImageFaces)
{
Debug.WriteLine(item);
}
glControl.MakeCurrent();
glControl.VSync = true;
glControl.Resize += GlControl_Resize;
glControl.Paint += GlControl_Paint;
shader = new Shader("Shaders/shader.vert", "Shaders/shader.frag");
shader.SetInt("cubeMapArray", 0);
// We initialize the camera so that it is 3 units back from where the rectangle is
// and give it the proper aspect ratio
camera = new Camera(Vector3.UnitZ, glControl.AspectRatio);
}
private void GlControl_Paint(object sender, PaintEventArgs e)
{
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_vertexBufferObject = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject);
GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Length * sizeof(float), _vertices, BufferUsageHint.StaticDraw);
cubemap = new TextureCubemap(ImageFaces);
cubemap.UseCubemap();
_vertexArrayObject = GL.GenVertexArray();
GL.BindVertexArray(_vertexArrayObject);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject);
//GL.BindBuffer(BufferTarget.ElementArrayBuffer, _elementBufferObject);
var vertexLocation = shader.GetAttribLocation("aPos");
GL.EnableVertexAttribArray(vertexLocation);
GL.VertexAttribPointer(vertexLocation, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
//// Next, we also setup texture coordinates. It works in much the same way.
//// We add an offset of 3, since the first vertex coordinate comes after the first vertex
//// and change the amount of data to 2 because there's only 2 floats for vertex coordinates
//var texCoordLocation = shader.GetAttribLocation("TexCoords");
//GL.EnableVertexAttribArray(texCoordLocation);
//GL.VertexAttribPointer(texCoordLocation, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), 3 * sizeof(float));
GL.Clear(ClearBufferMask.ColorBufferBit);
cubemap.UseCubemap();
shader.Use();
shader.SetMatrix4("view", camera.GetViewMatrix());
shader.SetMatrix4("projection", camera.GetProjectionMatrix());
GL.DrawArrays(PrimitiveType.Triangles, 0, 36);
glControl.SwapBuffers();
}
private readonly float[] _vertices =
{
// positions
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, -1.0f,
-1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f,
-1.0f, -1.0f, 1.0f,
1.0f, -1.0f, 1.0f
};
private void GlControl_Resize(object sender, EventArgs e)
{
InitializeView();
Debug.WriteLine("Resizing...");
}
private void WindowsFormsHost_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("WFH Loaded...");
SetupGLControl();
}
public void InitializeView()
{
double newWidth = glControl.ClientSize.Width;
double newHeight = glControl.ClientSize.Height;
GL.Viewport(0, 0, (int)newWidth, (int)newHeight);
// We enable depth testing here. If you try to draw something more complex than one plane without this,
// you'll notice that polygons further in the background will occasionally be drawn over the top of the ones in the foreground.
// Obviously, we don't want this, so we enable depth testing. We also clear the depth buffer in GL.Clear over in OnRenderFrame.
GL.Enable(EnableCap.DepthTest);
glControl.SwapBuffers();
}
}
}
TextureCubemap.cs
using OpenTK.Graphics.OpenGL;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PixelFormat = OpenTK.Graphics.OpenGL.PixelFormat;
namespace OpenTKTesting
{
class TextureCubemap
{
public readonly int Handle;
// Create texture from path.
public TextureCubemap(List<string> imagePaths)
{
// Generate handle
Handle = GL.GenTexture();
// Bind the handle
UseCubemap();
for (int i = 0; i < imagePaths.Count; i++)
{
// Load the image
using (var image = new Bitmap(imagePaths[i]))
{
Debug.WriteLine(imagePaths[i]);
var data = image.LockBits(
new Rectangle(0, 0, image.Width, image.Height),
ImageLockMode.ReadOnly,
System.Drawing.Imaging.PixelFormat.Format32bppRgb);
GL.TexImage2D(TextureTarget.TextureCubeMap,
0,
PixelInternalFormat.Rgb,
image.Width,
image.Height,
0,
PixelFormat.Rgb,
PixelType.UnsignedByte,
data.Scan0);
}
}
GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapS, (int)TextureParameterName.ClampToEdge);
GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapT, (int)TextureParameterName.ClampToEdge);
GL.TexParameter(TextureTarget.TextureCubeMap, TextureParameterName.TextureWrapR, (int)TextureParameterName.ClampToEdge);
}
public void UseCubemap(TextureUnit unit = TextureUnit.Texture0)
{
GL.ActiveTexture(unit);
GL.BindTexture(TextureTarget.TextureCubeMap, Handle);
}
}
}
Shader.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;
namespace OpenTKTesting
{
// A simple class meant to help create shaders.
public class Shader
{
public readonly int Handle;
private readonly Dictionary<string, int> _uniformLocations;
// This is how you create a simple shader.
// Shaders are written in GLSL, which is a language very similar to C in its semantics.
// The GLSL source is compiled *at runtime*, so it can optimize itself for the graphics card it's currently being used on.
// A commented example of GLSL can be found in shader.vert
public Shader(string vertPath, string fragPath)
{
// There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders.
// The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader.
// The vertex shader won't be too important here, but they'll be more important later.
// The fragment shader is responsible for then converting the vertices to "fragments", which represent all the data OpenGL needs to draw a pixel.
// The fragment shader is what we'll be using the most here.
// Load vertex shader and compile
// LoadSource is a simple function that just loads all text from the file whose path is given.
var shaderSource = LoadSource(vertPath);
// GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created.
var vertexShader = GL.CreateShader(ShaderType.VertexShader);
// Now, bind the GLSL source code
GL.ShaderSource(vertexShader, shaderSource);
// And then compile
CompileShader(vertexShader);
// We do the same for the fragment shader
shaderSource = LoadSource(fragPath);
var fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(fragmentShader, shaderSource);
CompileShader(fragmentShader);
// These two shaders must then be merged into a shader program, which can then be used by OpenGL.
// To do this, create a program...
Handle = GL.CreateProgram();
// Attach both shaders...
GL.AttachShader(Handle, vertexShader);
GL.AttachShader(Handle, fragmentShader);
// And then link them together.
LinkProgram(Handle);
// When the shader program is linked, it no longer needs the individual shaders attacked to it; the compiled code is copied into the shader program.
// Detach them, and then delete them.
GL.DetachShader(Handle, vertexShader);
GL.DetachShader(Handle, fragmentShader);
GL.DeleteShader(fragmentShader);
GL.DeleteShader(vertexShader);
// The shader is now ready to go, but first, we're going to cache all the shader uniform locations.
// Querying this from the shader is very slow, so we do it once on initialization and reuse those values
// later.
// First, we have to get the number of active uniforms in the shader.
GL.GetProgram(Handle, GetProgramParameterName.ActiveUniforms, out var numberOfUniforms);
// Next, allocate the dictionary to hold the locations.
_uniformLocations = new Dictionary<string, int>();
// Loop over all the uniforms,
for (var i = 0; i < numberOfUniforms; i++)
{
// get the name of this uniform,
var key = GL.GetActiveUniform(Handle, i, out _, out _);
// get the location,
var location = GL.GetUniformLocation(Handle, key);
// and then add it to the dictionary.
_uniformLocations.Add(key, location);
}
}
private static void CompileShader(int shader)
{
// Try to compile the shader
GL.CompileShader(shader);
// Check for compilation errors
GL.GetShader(shader, ShaderParameter.CompileStatus, out var code);
if (code != (int)All.True)
{
// We can use `GL.GetShaderInfoLog(shader)` to get information about the error.
throw new Exception($"Error occurred whilst compiling Shader({shader})");
}
}
private static void LinkProgram(int program)
{
// We link the program
GL.LinkProgram(program);
// Check for linking errors
GL.GetProgram(program, GetProgramParameterName.LinkStatus, out var code);
if (code != (int)All.True)
{
// We can use `GL.GetProgramInfoLog(program)` to get information about the error.
throw new Exception($"Error occurred whilst linking Program({program})");
}
}
// A wrapper function that enables the shader program.
public void Use()
{
GL.UseProgram(Handle);
}
// The shader sources provided with this project use hardcoded layout(location)-s. If you want to do it dynamically,
// you can omit the layout(location=X) lines in the vertex shader, and use this in VertexAttribPointer instead of the hardcoded values.
public int GetAttribLocation(string attribName)
{
return GL.GetAttribLocation(Handle, attribName);
}
// Just loads the entire file into a string.
private static string LoadSource(string path)
{
using (var sr = new StreamReader(path, Encoding.UTF8))
{
return sr.ReadToEnd();
}
}
// Uniform setters
// Uniforms are variables that can be set by user code, instead of reading them from the VBO.
// You use VBOs for vertex-related data, and uniforms for almost everything else.
// Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method:
// 1. Bind the program you want to set the uniform on
// 2. Get a handle to the location of the uniform with GL.GetUniformLocation.
// 3. Use the appropriate GL.Uniform* function to set the uniform.
/// <summary>
/// Set a uniform int on this shader.
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
public void SetInt(string name, int data)
{
GL.UseProgram(Handle);
GL.Uniform1(_uniformLocations[name], data);
}
/// <summary>
/// Set a uniform float on this shader.
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
public void SetFloat(string name, float data)
{
GL.UseProgram(Handle);
GL.Uniform1(_uniformLocations[name], data);
}
/// <summary>
/// Set a uniform Matrix4 on this shader
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
/// <remarks>
/// <para>
/// The matrix is transposed before being sent to the shader.
/// </para>
/// </remarks>
public void SetMatrix4(string name, Matrix4 data)
{
GL.UseProgram(Handle);
GL.UniformMatrix4(_uniformLocations[name], true, ref data);
}
/// <summary>
/// Set a uniform Vector3 on this shader.
/// </summary>
/// <param name="name">The name of the uniform</param>
/// <param name="data">The data to set</param>
public void SetVector3(string name, Vector3 data)
{
GL.UseProgram(Handle);
GL.Uniform3(_uniformLocations[name], data);
}
}
}
Camera.cs
using OpenTK;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenTKTesting
{
// This is the camera class as it could be set up after the tutorials on the website
// It is important to note there are a few ways you could have set up this camera, for example
// you could have also managed the player input inside the camera class, and a lot of the properties could have
// been made into functions.
// TL;DR: This is just one of many ways in which we could have set up the camera
// Check out the web version if you don't know why we are doing a specific thing or want to know more about the code
public class Camera
{
// Those vectors are directions pointing outwards from the camera to define how it rotated
private Vector3 _front = -Vector3.UnitZ;
private Vector3 _up = Vector3.UnitY;
private Vector3 _right = Vector3.UnitX;
// Rotation around the X axis (radians)
private float _pitch;
// Rotation around the Y axis (radians)
private float _yaw = -MathHelper.PiOver2; // Without this you would be started rotated 90 degrees right
// The field of view of the camera (radians)
private float _fov = MathHelper.PiOver2;
public Camera(Vector3 position, float aspectRatio)
{
Position = position;
AspectRatio = aspectRatio;
}
// The position of the camera
public Vector3 Position { get; set; }
// This is simply the aspect ratio of the viewport, used for the projection matrix
public float AspectRatio { private get; set; }
public Vector3 Front => _front;
public Vector3 Up => _up;
public Vector3 Right => _right;
// We convert from degrees to radians as soon as the property is set to improve performance
public float Pitch
{
get => MathHelper.RadiansToDegrees(_pitch);
set
{
// We clamp the pitch value between -89 and 89 to prevent the camera from going upside down, and a bunch
// of weird "bugs" when you are using euler angles for rotation.
// If you want to read more about this you can try researching a topic called gimbal lock
var angle = MathHelper.Clamp(value, -89f, 89f);
_pitch = MathHelper.DegreesToRadians(angle);
UpdateVectors();
}
}
// We convert from degrees to radians as soon as the property is set to improve performance
public float Yaw
{
get => MathHelper.RadiansToDegrees(_yaw);
set
{
_yaw = MathHelper.DegreesToRadians(value);
UpdateVectors();
}
}
// The field of view (FOV) is the vertical angle of the camera view, this has been discussed more in depth in a
// previous tutorial, but in this tutorial you have also learned how we can use this to simulate a zoom feature.
// We convert from degrees to radians as soon as the property is set to improve performance
public float Fov
{
get => MathHelper.RadiansToDegrees(_fov);
set
{
var angle = MathHelper.Clamp(value, 1f, 45f);
_fov = MathHelper.DegreesToRadians(angle);
}
}
// Get the view matrix using the amazing LookAt function described more in depth on the web tutorials
public Matrix4 GetViewMatrix()
{
return Matrix4.LookAt(Position, Position + _front, _up);
}
// Get the projection matrix using the same method we have used up until this point
public Matrix4 GetProjectionMatrix()
{
return Matrix4.CreatePerspectiveFieldOfView(_fov, AspectRatio, 0.01f, 100f);
}
// This function is going to update the direction vertices using some of the math learned in the web tutorials
private void UpdateVectors()
{
// First the front matrix is calculated using some basic trigonometry
_front.X = (float)Math.Cos(_pitch) * (float)Math.Cos(_yaw);
_front.Y = (float)Math.Sin(_pitch);
_front.Z = (float)Math.Cos(_pitch) * (float)Math.Sin(_yaw);
// We need to make sure the vectors are all normalized, as otherwise we would get some funky results
_front = Vector3.Normalize(_front);
// Calculate both the right and the up vector using cross product
// Note that we are calculating the right from the global up, this behaviour might
// not be what you need for all cameras so keep this in mind if you do not want a FPS camera
_right = Vector3.Normalize(Vector3.Cross(_front, Vector3.UnitY));
_up = Vector3.Normalize(Vector3.Cross(_right, _front));
}
}
}
shader.vert
#version 330 core
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
gl_Position = projection * view * vec4(aPos, 1.0);
}
shader.frag
#version 330 core
out vec4 FragColor;
in vec3 texDir;
in vec3 TexCoords;
uniform samplerCube cubeMapArray;
void main()
{
FragColor = texture(cubeMapArray, TexCoords);
}
редактирует:
@ Rabbid76 - Я сделал обновления, как вы предложили. К сожалению, я все еще получаю черный экран. Я более или менее следую вашим предложениям. Вы можете предложить что-нибудь еще, возможно, связанное с шейдерами или камерой?
private void SetupGLControl()
{
if (glControl == null)
{
return;
}
GetImageFaces();
foreach (var item in ImageFaces)
{
Debug.WriteLine(item);
}
glControl.MakeCurrent();
glControl.VSync = true;
glControl.Resize += GlControl_Resize;
glControl.Paint += GlControl_Paint;
shader = new Shader("Shaders/shader.vert", "Shaders/shader.frag");
shader.SetInt("cubeMapArray", 0);
// We initialize the camera so that it is 3 units back from where the rectangle is
// and give it the proper aspect ratio
camera = new Camera(Vector3.UnitZ, glControl.AspectRatio);
cubemap = new TextureCubemap(ImageFaces);
_vertexBufferObject = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject);
GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Length * sizeof(float), _vertices, BufferUsageHint.StaticDraw);
_vertexArrayObject = GL.GenVertexArray();
GL.BindVertexArray(_vertexArrayObject);
GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject);
var vertexLocation = shader.GetAttribLocation("aPos");
GL.EnableVertexAttribArray(vertexLocation);
GL.VertexAttribPointer(vertexLocation, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
}
private void GlControl_Paint(object sender, PaintEventArgs e)
{
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
cubemap.UseCubemap();
shader.Use();
shader.SetMatrix4("view", camera.GetViewMatrix());
shader.SetMatrix4("projection", camera.GetProjectionMatrix());
GL.BindVertexArray(_vertexArrayObject);
GL.DrawArrays(PrimitiveType.Triangles, 0, 36);
glControl.SwapBuffers();
}