Добро пожаловать на страницы руководства Open Toolkit
Прочитав руководство, вы научитесь успешно применять возможности OpenTK, начиная от настройки первого проекта и заканчивая распространением готового приложения. Также здесь можно найти рекомендации по созданию производительного кода и тому как сохранить условие кросс-платформенной совместимости.
Совершенно не обязательно читать руководство от первой до последней главы. Вы можете свободно пропускать целые главы и потом вернуться к ним в следующий раз. Имейте в виду, что всегда можно добавить комментарий в любом месте руководства – обратная связь поможет нам вместе сделать это руководство лучше, что принесет пользу не только Вам, но и будущим пользователям OpenTK.
Дополнительную информацию можно получить на OpenTK API reference; кроме того, форум содержит много примеров решения конкретных задач.
Надеемся, что время проведенное за чтением принесет большую пользу.
Введение
Достоинство VBO заключается в том, что мы можем заставить OpenGL сохранять информацию используемую для рисования (координаты, цвет, текстуры и нормали) непосредственно в памяти видеокарты, вместо того чтобы хранить их в системной памяти и передавать напрямую в видеокарту, когда нам это нужно. Конечно, это можно сделать и с помощью дисплейных списков (display list), но по сравнению с ними применяя VBO можно получить указатель на данные в видеопамяти и в случае необходимости изменять эти данные. Такой подход приводит к действительно огромному повышению производительности для динамических массивов графических примитивов и на протяжении многих лет является лучшим решением для хранения статических и динамических массивов (meshes).
Создаём буфер
Работа с буферными объектами похожа на работу с текстурами. Сначала создаем/удаляем указатель на объект, связываем его с существующими данными или заполняем новыми данными. В этом уроке нам понадобиться два объекта. Первый это VBO (Вершинный Буфер) содержащий информацию о вершинах (в нашем случае это координаты и цвет) и IBO (Индексный Буфер) ссылающийся на данные из VBO для индексации вершин треугольников. После создания VBO и IBO мы можем отрисовать всю фигуру с помощью одного вызова GL.DrawElements().
Сначала создаем два объекта:
uint[] VBOid = new uint[ 2 ]; GL.GenBuffers( 2, out VBOid );
Маловероятно, но возможно что OpenGL не хватит памяти для загрузки данных или то, что требуемое расширение не поддерживается. Это можно проверить вызовом GL.GetError().
Удаляем буфер
Драйвер OpenGL удаляет наши массивы вершин и прочую информацию при удалении контекста воспроизведения. Несмотря на это, удалить самостоятельно данные после использования это хорошая идея. Это можно следующим путем:
GL.DeleteBuffers( 2, ref VBOid );
Назначение буфера
Для того, чтобы OpenGL понять какую роль выполняет созданный буфер, необходимо пометить его как BufferTarget.ArrayBuffer или BufferTarget.ElementArrayBuffer. Первый тип используется для хранения координат, uv-координат, нормалей и так далее (VBO). Второй тип хранит индексы вершин (IBO).
GL.BindBuffer( BufferTarget.ArrayBuffer, VBOid[ 0 ] ); GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 1 ] );
Не обязательно всегда создавать вершинный и индексный буфер. Для примера, вы можете сохранить вершины в VBO буфере, а индексы держать в системной памяти. Также, необязательно связывать вершинный и индексный буфер вместеe, например, для одного вершинного буфера можно иметь два разных индексных буфера.
Есть две вещи о которых нельзя забывать:
1) Перед работой с VBO, требуется активировать GL.EnableClientState(EnableCap.VertexArray). Если используются нормали - GL.EnableClientState(EnableCap.NormalArray), это похоже на работу с классическими массивами вершин.
2) Все команды связанные с массивами вершин будут применяться к объектным буферам. Для отключения VBO, назначьте вершинному и индексному буферу значение ноль
GL.BindBuffer( BufferTarget.ArrayBuffer, 0 ); GL.BindBuffer( BufferTarget.ElementArrayBuffer, 0 );
Заполнение данными
Есть несколько путей, чтобы заполнить объектный буфер данными. Здесь мы покажем пример использования GL.BufferData вместе с прямым доступ к видеопамяти. После того как вы достаточно хорошо освоитесь с GL.BufferData можно использовать и GL.BufferSubData.
We make sure the correct object is bound (it is not required to do this, if the buffer is already bound. Just here to clarify on which object we currently work on)
GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 1 ] );
In the example application ushort has been used for Indices, because 16 Bits [0..65535] are more available Vertices than used by most real-time rendered meshes, however the mesh could index way more Vertices using a type like uint. Using ushort, OpenGL will store this data as 2 Bytes per index, saving memory compared to a 4 Bytes UInt32 per index.
The function GL.BufferData's first parameter is the target we want to use, the second is the amount of memory (in bytes) we need allocated to hold all our data. The third parameter is pointing at the data we wish to send to the graphics card, this can be IntPtr.Zero and you may send the data at a later stage with GL.MapBuffer (more about this later). The last parameter is an optimization hint for the driver, it will place your data in the best suited place for your purposes.
GL.BufferData( BufferTarget.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), Indices, BufferUsageHint.StaticDraw );
That's all, OpenGL now has a copy of Indices available and we could dispose the array, assuming we have the Index Count of the array stored in a variable for the draw call later on.
Now that we've stored the indices in an IBO, the Vertices are next. Again, we make sure the binding is correct, give a pointer to the Vertex count, and finally the usage hint.
GL.BindBuffer( BufferTarget.ArrayBuffer, VBOid[ 0 ] ); GL.BufferData( BufferTarget.ArrayBuffer, (IntPtr) ( Vertices.Length * 8 * sizeof( float ) ), Vertices, BufferUsageHint.StaticDraw );
There's a table at the bottom of this page, explaining the options in the enum BufferUsageHint in more detail.
While the first described technique to pass data into the objects required a copy of the data in system memory, this alternative will give us a pointer to the video memory reserved by the object. This is useful for dynamic models that have no copy in client memory that could be used by GL.BufferData, since you wish to rebuild it every single frame (e.g. fully procedural objects, particle system).
First we make sure that we got the desired object bound and reserve memory, the pointer towards the Indices is actually IntPtr.Zero, because we only need an empty buffer.
GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 0 ] ); GL.BufferData( BufferTarget.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), IntPtr.Zero, BufferUsageHint.StaticDraw );
Note that you should change BufferUsageHint.StaticDraw properly according to what you intend to do with the Data, there's a table at the bottom of this page. Now we're able to request a pointer to the video memory.
IntPtr VideoMemoryIntPtr = GL.MapBuffer(BufferTarget.ElementArrayBuffer, BufferAccess.WriteOnly);
Valid access flags for the pointer are BufferAccess.ReadOnly, BufferAccess.WriteOnly or BufferAccess.ReadWrite, which help the driver understand what you're going to do with the data. Note that the data's object is locked until we unmap it, so we want to keep the timespan over which we use the pointer as short as possible. We may now write some data into the buffer, once we're done we must release the lock.
unsafe { fixed ( ushort* SystemMemory = &Indices[0] ) { ushort* VideoMemory = (ushort*) VideoMemoryIntPtr.ToPointer(); for ( int i = 0; i < Indices.Length; i++ ) VideoMemory[ i ] = SystemMemory[ i ]; // simulate what GL.BufferData would do } } GL.UnmapBuffer( BufferTarget.ElementArrayBuffer );
The pointer is now invalid and may not be stored for future use, if we wish to modify the object again, we have to call GL.MapBuffer again.
Further reading
Visit this link in order to tell OpenGL about the composition of your Vertex data, and this link for drawing the data.
Optimization:
One hint from the nVidia whitepaper was regarding the situation, if we want to update all data in the buffer object by using GL.MapBuffer and not retrieve any of the old data. Although this is a bad idea, because mapping the buffer is a more expensive operation than just calling GL.BufferData, it might be necessary in cases where you have no copy of the data in system memory, but build it on the fly. The solution to making this somewhat efficient is first calling GL.BufferData with a IntPtr.Zero again, which tells the driver that the old data isn't valid anymore. Calling GL.MapBuffer will return a new pointer to a valid memory location of the requested size to write to, while the old data will be cleaned up once it's not used in any draw operations anymore.
Also note that either reading from a VBO or wrapping it into a Display List is very slow and should both be avoided.
Table 1:
BufferUsageHint.Static... Assumed to be a 1-to-n update-to-draw. Means the data is specified once (during initialization).
BufferUsageHint.Dynamic... Assumed to be a n-to-n update-to-draw. Means the data is drawn multiple times before it changes.
BufferUsageHint.Stream... Assumed to be a 1-to-1 update-to-draw. Means the data is very volatile and will change every frame.
...Draw Means the buffer will be used to sending data to GPU. video memory (Static|StreamDraw) or AGP (DynamicDraw)
...Read Means the data must be easy to access, will most likely be system or AGP memory.
...Copy Means we are about to do some ..Read and ..Draw operations.
...
Начнем с подготовки IBO, здесь нет разницы с чего начинать первыми. Для простоты сделаем то, что быстрее всего.
Мы должны удостоверитьтся в том что правильно связали объекты (этого не требуется если объекты уже были связаны. Здесь мы просто вносим ясность с каким объектом ведется работа)
GL.BindBuffer( Version15.ElementArrayBuffer, VBOid[ 1 ] );
В этом примере используется тип ushort для граней, потому максимальное значение для этого типа 65535 больше чем используемое нами Сторон. При использовании настоящих наборов это число может быть и больше и поэтому стоит задумать об использовании типа наподобие uint. В данном случае используя ushort мы экономим два байта на каждый индекс как если бы мы использовали тип UInt32.
У функции GL.BufferData первый параметр это назначение по которому мы хотим использовать, второй это количество памяти (в байтах) которое мы хотим использовать для хранения своих данных. Третий параметр это указатель на данные которые мы хотим отправить видеокарте, его тип должен быть IntPtr (целочисленный указатель). Здесь можно использовать IntPtr.Zero указатель на ноль если мы заполним данные на последующем этапе с помощью GL.MapBuffer (подробнее об этом позже). Последний параметр это подсказка драйверу видеокарте об оптимизации, это поможет расположить твои данные в наилучшем месте по твоему желанию. В таблице 1 приведен полный список опций.
GL.BufferData( Version15.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), Indices, Version15.StaticDraw );
На этом всё, OpenGL скопирует доступные грани и мы можем освободить массив на который ссылаясь. Теперь у нас есть Массив Индексов который можно использовать позже.
Итак, мы сохранили индисы в IBO, теперь пора считать Вершины. Еще раз убедимся что связали всё правильно и окончим оптимизацией.
GL.BindBuffer( Version15.ArrayBuffer, VBOid[ 0 ] );
GL.BufferData( Version15.ArrayBuffer, (IntPtr) ( Vertices.Length * 6 * sizeof( float ) ), Vertices, Version15.StaticDraw );
Итак мы сохранили всю информацию о графике и перед вызовом GL.DrawElements должны кое что сделать. Вершины которые мы хотим использовать содержат не только координаты, но и цвета с которыми будут отображаться. GL.InterleavedArrays позволяет включить/выключить необходимое состояние клиента для правильной интерпретации наших загруженных данных OpenGL , первый параметр говорит о том что у нас есть 3 float для описания цвета (C3F) и 3 float для описания позиции (V3F). Второй параметр это шаг с которым должен быть совершен переход от первой вершины ко второй, третьей и так далее. Последний параметр должен быть указатель на Indeces, но мы уже загрузили его в видеопамять, поэтому второй раз указывать не нужно.
GL.InterleavedArrays( InterleavedArrayFormat.C3fV3f, 0, null );
Преимущество этой команды в том что она позволяет ясно объяснить OpenGL с какие данные нужно выводить и также это позволяет оптимизировать память. Запомни что GL.InterleavedArrays сработает только если включены EnableCap.VertexArray, EnableCap.ColorArray или изменялись GL.VertexPointer или GL.ColorPointer. Незабудь включить их обратно иначе ничего не произойдет.
Для более детального описания о состояниях Массива Вершин обращайся к RedBook или к описанию команд OpenGL.
Сначала нужно создать имена буферным объектам:
VBOid = new int[2]; GL.GenBuffers(2, VBOid);
Существует два буферных объекта: для хранения массива вершин (ArrayBuffer) и для хранения массива индексов (ElementArrayBuffer).
Для этих буферов мы создали два имени.
Теперь связываем имена с буферными объектами:
GL.BindBuffer(BufferTarget.ArrayBuffer, VBOid[0]); //связываем VBO GL.BindBuffer(BufferTarget.ElementArrayBuffer, VBOid[1]); //связываем IBO
Нам необходимо создать в нужном формате два массива для хранения информации о вершинах и индексах, которые мы передадим в только что созданные буферные объекты. Какой именно будет формат, решать Вам. Возможно, для быстрой загрузки Вы захотите хранить объекты в файлах в этом формате.
В массиве для вершин минимально требуется только координаты вершин (x, y, z). Но, если требуются нормали и текстурные координаты, то обычно можно увидеть данные в таком виде:

sizeof(float) * 3 для указания позиции вершины, sizeof(float) * 3 на вектор нормали и sizeof(float) * 2 на текстурные координаты. Если на машине размер типа float равен 4 байтам, то на описание точки требуется 4*3 + 4*3 + 4*2 = 4*8 =32 байта. Всего размер массива будет (sizeof(float) * 8) * vertex_count.
Массив индексов состоит из целочисленных данных типа int. Каждый элемент индекса указывает на блок данных из массива вершин. Т.к. массив вершин одномерный, то позиция блока с нужными данными находится по адресу index * sizeof(float) * 8.
Передаём буферу вершин указатель на массив вершин, в котором находятся данные в нашем формате (массив с именем, например, "vertexes"):
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertexes.Length * sizeof(float)), vertexes, BufferUsageHint.StaticDraw); //записываем в память VBO
Данные из массива вершин перемещаются в т.н. "хранилище данных". Параметр-подсказка BufferUsageHint со значением StaticDraw "подсказывает" машине, что данные будут меняться редко.
Точно также передаём буферу индексов указатель на массив индексов (с именем, например, "indexes"):
GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indexes.Length * sizeof(int)), indexes, BufferUsageHint.StaticDraw); //записываем в память IBO
Данные переданы в буфер. Обходя буфер индексов, OpenGL будет искать данные в буфере вершин, и теперь нужно указать, по какому адресу искать данные о позициях точек, нормалей и текстурных координат:
GL.VertexPointer(3, VertexPointerType.Float, sizeof(float) * 8, 0); //координаты точек идут первыми, смещение 0 GL.NormalPointer(NormalPointerType.Float, sizeof(float) * 8, sizeof(float) * 3); //вектор нормали идет после данных о позиции GL.TexCoordPointer(2, TexCoordPointerType.Float, sizeof(float) * 8, sizeof(float) * 6); //текстурные координаты после вектора нормали
Эти функции указывают расстояние между данными и начальную позицию данных в буфере вершин. Расстояние одинаковое sizeof(float) * 8 (длина блока данных о вершине), а начальная позиция зависит от положения внутри блока.
Осталось указать OpenGL, что мы в данный момент хотим вывести на экран всю информацию о точках:
GL.EnableClientState(ArrayCap.VertexArray); GL.EnableClientState(ArrayCap.NormalArray); GL.EnableClientState(ArrayCap.TextureCoordArray);
Эти команды можно давать в каждом кадре.
И в каждом кадре даем команду рисования:
GL.DrawElements(BeginMode.Triangles, indexes.Length, DrawElementsType.UnsignedInt, IntPtr.Zero);
Эта функция находит буфер индексов, берет каждые 3 индекса для рисования треугольников (т.к. мы указали BeginMode.Triangles), находит по ним 3 блока данных из буфера вершин, по данным о позициях (из функций VertexPointer, NormalPointer и TexCoordPointer) берет нужные данные из буфера вершин, рисует.
Для использования OpenTK требуется .Net 2.0 runtime (Windows) или последняя версия 1.2.x Mono release (Linux/Mac OS X/Windows). При использовании Mono, настоятельно рекомендуем обновиться до последней доступной версии - версии младше, чем 1.2.6, содержат серьезные ошибки, которые могут повлиять на стабильность OpenTK.
Если Вы планируете скомпилировать библиотеку OpenTK самостоятельно из исходных текстов, то для этого Вам понадобиться Nant (в дальнейшем эта зависимость будет удалена)
Последнее и, наверное, самое главное, не забудьте посетить страницу проекта и скачать последнюю версию OpenTK. OpenTK распространяется в виде zip/7z – архива. Установка заключается в извлечении содержимого архива на Ваш жесткий диск. На данный момент не возможно инсталлировать OpenTK в глобальный кеш сборок (GAC).
Для использования OpenTK, необходимо чтобы Ваш проект ссылался на OpenTK.dll. Также поместите файл OpenTK.dll.config в папку проектом для достижения совместимости «скомпилировал один раз – запустил везде».
Следующие страницы посвящены более детальным инструкциям.
Что такое OpenTK?
Простыми словами, Open Toolkit это бесплатный проект, который позволяет использовать OpenGL, OpenGL|ES, OpenCL и OpenAL APIs из управляемых языков.
OpenTK начинался как экспериментальное ответвление от "Tao framework" в начале лета 2006 года. Оригинальной целью было создать более чистую обертку (wrapper) чем Tao.OpenGL, однако достаточно быстро появилась другая задача - обеспечить доступ к начальной инициализации Khronos и Creative API. В этом смысле Open Toolkit похож на такие проекты, как Tao, SlimDX, SDL или GLFW.
Однако в отличии от этих библиотек OpenTK значительно сосредоточен на формировании удобного интерфейса. Вместо указателей, OpenTK предоставляет обобщенные классы (generic). Вместо простых констант, OpenTK использует строго-типизированные перечисления. Вместо обычного списка функций, OpenTK разделяет функции по категориям. Также в состав OpenTK включена достаточно обширная Математическая библиотека, которая может быть использована из любого API.
Итак, перечислим достоинства:
Open Toolkit идеально подходит для создания приложений требовательных к вычислительным ресурсам и использующим сложную графику (звук), такие как игры или научные программы визуализации. Лицензия позволяет использовать библиотеку как для свободных так и коммерческих приложений.
Чтобы помочь вам писать более чистый код, OpenTK дополняет стандартные скалярные типы векторами, кватернионами и матрицами. Они более удобны в использовании чем обычные массивы.
Приведем пример, как можно задать параметры GL.Color() различными путями.
GL.Color( 0.0f, 1.0f, 1.0f ); Vector3 MyColor = new Vector3( 0.0f, 1.0f, 1.0f ); GL.Color( MyColor ); // требует подключения System.Drawing library GL.Color( Color.Cyan );
Подобная перегрузка функций используется повсеместно в OpenTK.Graphics и OpenTK.Audio.
Обзор
Приведенные ниже типы относятся к безопасным типам. Программы использующие эти типы будет работать на всех поддерживаемых платформах.
Vectors
Half - новый скалярный 16-битный тип данных. Больше информации можно найти здесь.Vector2h - 2-компонентный вектор типа Half.Vector3h - 3-компонентный вектор типа Half.Vector4h - 4-компонентный вектор типа Half.Single - стандартный скалярный 32 битный тип данных.Vector2 - 2-компонентный вектор типа Single.Vector3 - 3-компонентный вектор типа Single.Vector4 - 4-компонентный вектор типа Single.Double - стандартный скалярный 64 битный тип данных.Vector2d - 2-компонентный вектор типа Double.Vector3d - 3-компонентный вектор типа Double.Vector4d - 4-компонентный вектор типа Double.Quaternion
Quaternion - 4-компонентный тип с плавающей запятой одинарной точности.Quaterniond - 4-компонентный тип с плавающей запятой двойной точности.Row-Major Matrices
Matrix3d - матрица 3x3 двойной точности.Matrix4 - матрица 4x4 одинарной точности.Matrix4d - матрица 4x4 двойной точности.О реализации этих новых типов данных можно прочитать здесь.
Преобразования
Для симметрии к известным типам данных, все типы данных OpenTK.Math поддерживают преобразования (cast) и сериализацию.
Vector2d TexCoord = new Vector2d( 0.2, 0.5 ); Vector2h HalfTexCoord = (Vector2h)TexCoord; Vector3h Normal = (Vector3h)Vector3.UnitX;
Ссылочные и Статические Методы
Полный перечень всех методов каждой структуры данных слишком велик, чтобы познакомится со списком доступных функций используйте описание функции и встроенную документацию. Мы надеемся что вы уже знаете, что одна часть функциональности доступна для статических структур и другая часть для динамических.
Vector3 Normal = Vector3.UnitX; Normal.Normalize(); Vector4 TransformedVector = Vector4.Transform( Vector4.UnitX, Matrix4.Identity );
[Статья не закончена]
В этом руководстве рассказывается о том, как написать оконное приложение с помощью OpenTK.
Сперва добавить, потом перемешать
Начнем с того, что создадим пустую форму. Далее щелчком правой кнопки мыши, в любом пустом месте «ToolBox», откроем диалог «Choose Item…». Здесь необходимо нажать на кнопку «Browse» и указать путь к OpenTK.dll. Проверь, появился ли компонент «GLControl» на закладке «.NET Framework Components», так как это показано на рисунке.

После этого добавь компонент GLControl на твою форму, так же как и любой другой .NET компонент. На твоей форме появиться новый компонент GLControl с именем glControl1.
Сразу бросатеся в глаза то, что glControl1 содержит в себе «графический мусор», не думай, что это проблемы связаны с твоей видеокартой. Все дело в том, что под капотом GLControl происходит вызов GLContext (контекст отвечающий за прорисовку изображения), который создается только во время выполнения твоей программы.
Рецепт красоты.
Как уже было сказано, GLContext создается во время выполнения программы. Поэтому получить доступ к свойствам glControl1 можно только после создания GLContext. После запуска приложения, обратиться к glControl можно после того, как было обработано событие Load().
Выполнение команд начинающихся с GL.* возможен в любом месте программы (кроме конструктора формы).
Как проверить был ли создан контекст или нет? Одним из решением этого вопроса служит объявление переменой
bool loaded = false
которая принимает значение true, при обработке Load()
public partial class Form1 : Form { bool loaded = false; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { loaded = true; } }
Теперь, для того чтобы избавить себя от ошибки обращения к несуществующему контексту, обращением к glControl1, добавим следующие строчки
private void glControl1_Resize(object sender, EventArgs e) { if (!loaded) return; }
Заводим мотор. Минимальное приложение на OpenTK
Пришло время увидеть что-нибудь на экране монитора. В разделе Using добавляем ссылки на пространство имен библиотеки OpenTK и OpenTK.Enums и реализуем обработку события OnPaint()
using OpenTK.OpenGL; using OpenTK.OpenGL.Enums; ... private void glControl1_Paint(object sender, PaintEventArgs e) { if (!loaded) //Пока контекст не создан return; GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); glControl1.SwapBuffers(); }
Скомпилируй и запусти проект. То что ты видишь это настоящий "Черный Квадрат" от создателей OpenTK.

Теперь давайте добавим немного цвета. Лучшее место для размещения кода задающего «цвет очистки» - в обработке события OnLoad()
private void Form1_Load(object sender, EventArgs e) { loaded = true; GL.ClearColor(Color.SkyBlue); }

Немного настроек
Следующее шаг- это создание желтого треугольника. Для начала (если ты желаешь стать добропорядочным жителем страны OpenGL) установим ортографическую матрицу проекций используя GL.Ortho() и вызовем GL.Viewport().
Добавим новые строки в обработку события Load. Небольшое замечание - будем игнорировать изменение размера окна пользователем.
Для ясности код первоначальной настройки разделен на несколько частей
private void Form1_Load(object sender, EventArgs e) { loaded = true; GL.ClearColor(Color.SkyBlue); SetupViewport(); } private void SetupViewport() { int w = glControl1.Width; int h = glControl1.Height; GL.MatrixMode(MatrixMode.Projection); GL.LoadIdentity(); GL.Ortho(0, w, 0, h, -1, 1); // Верхний левый угол имеет кооординаты(0, 0) GL.Viewport(0, 0, w, h); // Использовать всю поверхность GLControl под рисование }
Между инструкциями Clear() и SwapBuffers() разместим код рисующий треугольник:
private void glControl1_Paint(object sender, PaintEventArgs e) { if (!loaded) return; GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.MatrixMode(MatrixMode.Modelview); GL.LoadIdentity(); GL.Color3(Color.Yellow); GL.Begin(BeginMode.Triangles); GL.Vertex2(10, 20); GL.Vertex2(100, 20); GL.Vertex2(100, 50); GL.End(); glControl1.SwapBuffers(); }
Поздравляю ! Теперь к стране OpenGL присоеднился новый гражданин.

Желание управлять
Теперь попробуем управлять треугольником с клавиатуры. Сделаем так, чтобы после нажатия на пробел, треугольник сдвигался на один пиксель вправо.
Для работы с клавиатурой возможно использовать два подхода - обрабабатывать событие Windows.Forms или использовать OpenTK KeyboardDevice. Если GLControl является небольшой частью твоего WinForms приложения, лучше обрабатать стандартное события WinForms.
Введем переменную int x=0; которая будет увеличиваться при наступлении события KeyDown, которое