Разработка 3D движка на C: от математики до оптимизации рендеринга
Для кого эта статья:
- Разработчики, интересующиеся созданием 3D графики и игрового движка
- Студенты и аспиранты в области компьютерных наук и программирования
Специалисты по тестированию ПО, желающие освоить графическое программирование
Создание 3D движка на языке C — это как собирать механические часы вручную в эпоху умных устройств. Такое мастерство даёт глубокое понимание принципов компьютерной графики и механизмов, скрытых под капотом современных игровых движков. В этом руководстве мы пройдём весь путь от математических основ до создания работающего 3D движка, способного рендерить трёхмерные объекты с освещением и текстурами. Готовы погрузиться в мир вершин, полигонов и шейдеров? 🚀
Разработка 3D движка требует системного мышления и внимания к деталям — качеств, необходимых любому специалисту по тестированию ПО. На Курсе тестировщика ПО от Skypro вы научитесь не только находить ошибки в сложных системах, но и понимать архитектуру приложений на глубинном уровне. Знания программирования и тестирования графических приложений откроют перед вами двери в геймдев и сферу 3D-моделирования.
Основы архитектуры 3D движка на языке C
Архитектура 3D движка — это набор компонентов, которые работают вместе для преобразования 3D моделей в пиксели на экране. Создавая движок на C, мы получаем полный контроль над низкоуровневыми операциями и максимальную производительность. 💻
Основные компоненты 3D движка включают:
- Система управления окнами — для создания контекста отрисовки и обработки ввода
- Математическая библиотека — для операций с векторами, матрицами и кватернионами
- Модуль рендеринга — сердце движка, отвечающее за отрисовку 3D объектов
- Система управления ресурсами — загрузка моделей, текстур и шейдеров
- Модуль освещения — расчёт освещённости сцены
- Система камер — управление перспективой и точкой зрения
Для реализации нашего движка будем использовать библиотеку GLFW для управления окнами и OpenGL для работы с графикой. Это значительно упрощает создание кросс-платформенного решения.
| Компонент | Библиотека | Назначение |
|---|---|---|
| Оконная система | GLFW | Создание окна, обработка ввода |
| Графический API | OpenGL | Рендеринг 3D объектов |
| Математика | GLM (или собственная) | Векторные и матричные вычисления |
| Загрузка моделей | Assimp (опционально) | Импорт 3D моделей из различных форматов |
| Загрузка текстур | stb_image | Декодирование изображений в форматах PNG, JPEG и др. |
Ключевым аспектом архитектуры является разделение ответственности между компонентами. Каждый модуль должен решать только свою задачу и иметь четко определенные интерфейсы взаимодействия с другими частями системы.
Александр Петров, Lead Graphics Programmer
Помню свой первый опыт создания 3D движка на C. Это был проект для университета, и я решил, что OpenGL — это слишком просто, поэтому попытался писать рендеринг с нуля. Две недели я бился над растеризацией треугольников, пытаясь оптимизировать алгоритмы до предела. Когда наконец заставил это работать, понял, что текстурирование и освещение займут ещё больше времени. В итоге вернулся к OpenGL, но тот опыт дал мне глубокое понимание того, что происходит "под капотом" графических API. Сегодня я рекомендую начинающим не изобретать велосипед, а использовать готовые решения для низкоуровневых операций, сосредоточившись на архитектуре и более высокоуровневых компонентах.
Вот базовая структура нашего проекта:
// engine.h – главный заголовочный файл
typedef struct {
// Состояние окна
GLFWwindow* window;
int width, height;
// Ресурсы
struct Model* models;
struct Texture* textures;
// Камера и сцена
struct Camera camera;
struct Light lights[MAX_LIGHTS];
// Рендеринг
struct Shader* shaders;
} Engine;
// Инициализация и завершение работы
Engine* engine_init(int width, int height, const char* title);
void engine_shutdown(Engine* engine);
// Основной цикл
void engine_run(Engine* engine);

Математический фундамент для программирования 3D графики
Математика — это язык, на котором говорит компьютерная графика. Без понимания векторов, матриц и тригонометрии создать 3D движок невозможно. 📐
Ключевые математические концепции, необходимые для 3D движка:
- Векторы — для представления позиций, направлений и цветов
- Матрицы — для трансформаций объектов в пространстве
- Кватернионы — для представления вращений без проблемы "шарнирного замка"
- Проекции — для преобразования 3D координат в 2D экранные координаты
Начнём с реализации базовых векторных операций:
// math_3d.h
typedef struct {
float x, y, z;
} Vec3;
// Сложение векторов
Vec3 vec3_add(Vec3 a, Vec3 b) {
Vec3 result = {a.x + b.x, a.y + b.y, a.z + b.z};
return result;
}
// Вычитание векторов
Vec3 vec3_sub(Vec3 a, Vec3 b) {
Vec3 result = {a.x – b.x, a.y – b.y, a.z – b.z};
return result;
}
// Скалярное произведение
float vec3_dot(Vec3 a, Vec3 b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
// Векторное произведение
Vec3 vec3_cross(Vec3 a, Vec3 b) {
Vec3 result = {
a.y * b.z – a.z * b.y,
a.z * b.x – a.x * b.z,
a.x * b.y – a.y * b.x
};
return result;
}
// Нормализация вектора
Vec3 vec3_normalize(Vec3 v) {
float length = sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
Vec3 result = {v.x / length, v.y / length, v.z / length};
return result;
}
Для работы с трансформациями необходимы матрицы 4x4:
typedef struct {
float m[4][4];
} Mat4;
// Создание единичной матрицы
Mat4 mat4_identity() {
Mat4 result = {{{
{1.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
{0.0f, 0.0f, 0.0f, 1.0f}
}}};
return result;
}
// Умножение матриц
Mat4 mat4_multiply(Mat4 a, Mat4 b) {
Mat4 result = {{{0}}};
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 4; j++) {
for(int k = 0; k < 4; k++) {
result.m[i][j] += a.m[i][k] * b.m[k][j];
}
}
}
return result;
}
Особенно важны матрицы трансформации — перемещения, вращения и масштабирования:
// Матрица перемещения
Mat4 mat4_translation(float x, float y, float z) {
Mat4 result = mat4_identity();
result.m[0][3] = x;
result.m[1][3] = y;
result.m[2][3] = z;
return result;
}
// Матрица вращения вокруг оси X
Mat4 mat4_rotation_x(float angle_rad) {
Mat4 result = mat4_identity();
float c = cos(angle_rad);
float s = sin(angle_rad);
result.m[1][1] = c;
result.m[1][2] = -s;
result.m[2][1] = s;
result.m[2][2] = c;
return result;
}
// Аналогично для осей Y и Z...
| Матрица | Назначение | Применение в движке |
|---|---|---|
| Модельная (Model) | Позиционирование объекта в мировом пространстве | Перемещение, вращение и масштабирование объектов |
| Видовая (View) | Позиционирование камеры | Определение точки зрения на сцену |
| Проекционная (Projection) | Преобразование 3D в 2D | Создание перспективы и глубины |
| MVP (Model-View-Projection) | Комбинирование всех трансформаций | Итоговое преобразование для отрисовки |
Создание рендеринг-цикла и настройка оконной системы
Рендеринг-цикл — это сердце нашего движка, которое поддерживает постоянное обновление и отображение 3D сцены. Для начала нужно создать окно и настроить OpenGL контекст. 🖼️
Инициализация GLFW и создание окна:
Engine* engine_init(int width, int height, const char* title) {
Engine* engine = malloc(sizeof(Engine));
// Инициализация GLFW
if (!glfwInit()) {
fprintf(stderr, "Failed to initialize GLFW\n");
free(engine);
return NULL;
}
// Настройка OpenGL
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Создание окна
engine->window = glfwCreateWindow(width, height, title, NULL, NULL);
if (!engine->window) {
fprintf(stderr, "Failed to create GLFW window\n");
glfwTerminate();
free(engine);
return NULL;
}
// Установка контекста
glfwMakeContextCurrent(engine->window);
// Инициализация GLAD для загрузки функций OpenGL
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
fprintf(stderr, "Failed to initialize GLAD\n");
glfwDestroyWindow(engine->window);
glfwTerminate();
free(engine);
return NULL;
}
// Настройка вьюпорта
glViewport(0, 0, width, height);
// Инициализация других подсистем...
return engine;
}
Теперь реализуем основной рендеринг-цикл:
void engine_run(Engine* engine) {
// Цикл выполняется, пока окно не закрыто
while (!glfwWindowShouldClose(engine->window)) {
// Очистка буфера
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Обработка ввода
process_input(engine);
// Обновление состояния сцены
update_scene(engine);
// Рендеринг объектов
render_scene(engine);
// Обмен буферов и обработка событий
glfwSwapBuffers(engine->window);
glfwPollEvents();
}
}
Максим Иванов, Game Engine Developer
Однажды я разрабатывал 3D движок для визуализации архитектурных проектов. Клиенту нужно было, чтобы пользователи могли "гулять" по зданию в режиме реального времени. Я настроил простой рендеринг-цикл, но обнаружил странную проблему: движение было плавным на одних компьютерах и дёрганым на других. Оказалось, что я привязал скорость перемещения камеры к частоте кадров. На мощных машинах с 120+ FPS объекты двигались вдвое быстрее, чем на стандартных с 60 FPS! Решением стало введение временной дельты между кадрами: каждое перемещение умножалось на время, прошедшее с предыдущего кадра. Это важный урок: никогда не привязывайте логику движения к частоте рендеринга.
Важно правильно обрабатывать пользовательский ввод:
void process_input(Engine* engine) {
if(glfwGetKey(engine->window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(engine->window, 1);
// Управление камерой
if(glfwGetKey(engine->window, GLFW_KEY_W) == GLFW_PRESS)
camera_move_forward(&engine->camera);
if(glfwGetKey(engine->window, GLFW_KEY_S) == GLFW_PRESS)
camera_move_backward(&engine->camera);
// И так далее для других клавиш...
}
Для рендеринга объектов необходимо создать буферы вершин и индексов:
typedef struct {
unsigned int VAO, VBO, EBO;
int vertex_count, index_count;
} Mesh;
Mesh* create_mesh(float* vertices, int vertex_count,
unsigned int* indices, int index_count) {
Mesh* mesh = malloc(sizeof(Mesh));
mesh->vertex_count = vertex_count;
mesh->index_count = index_count;
// Создание буферов
glGenVertexArrays(1, &mesh->VAO);
glGenBuffers(1, &mesh->VBO);
glGenBuffers(1, &mesh->EBO);
// Привязка VAO
glBindVertexArray(mesh->VAO);
// Загрузка вершин
glBindBuffer(GL_ARRAY_BUFFER, mesh->VBO);
glBufferData(GL_ARRAY_BUFFER, vertex_count * sizeof(float),
vertices, GL_STATIC_DRAW);
// Загрузка индексов
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh->EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
index_count * sizeof(unsigned int),
indices, GL_STATIC_DRAW);
// Настройка атрибутов вершин
// Позиции
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Нормали
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
8 * sizeof(float),
(void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Текстурные координаты
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE,
8 * sizeof(float),
(void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
// Отвязка VAO
glBindVertexArray(0);
return mesh;
}
Реализация ключевых модулей: камера, освещение, текстуры
Теперь создадим основные модули, необходимые для полноценного 3D движка: камеру для просмотра сцены, систему освещения и работу с текстурами. 🔦
Начнем с реализации камеры:
typedef struct {
Vec3 position;
Vec3 front;
Vec3 up;
Vec3 right;
float yaw;
float pitch;
} Camera;
void camera_init(Camera* camera) {
camera->position = (Vec3){0.0f, 0.0f, 3.0f};
camera->front = (Vec3){0.0f, 0.0f, -1.0f};
camera->up = (Vec3){0.0f, 1.0f, 0.0f};
camera->right = (Vec3){1.0f, 0.0f, 0.0f};
camera->yaw = -90.0f;
camera->pitch = 0.0f;
}
void camera_update_vectors(Camera* camera) {
// Вычисляем новый вектор направления
Vec3 front;
front.x = cos(to_radians(camera->yaw)) * cos(to_radians(camera->pitch));
front.y = sin(to_radians(camera->pitch));
front.z = sin(to_radians(camera->yaw)) * cos(to_radians(camera->pitch));
camera->front = vec3_normalize(front);
// Пересчитываем правый и верхний векторы
camera->right = vec3_normalize(vec3_cross(camera->front, (Vec3){0.0f, 1.0f, 0.0f}));
camera->up = vec3_normalize(vec3_cross(camera->right, camera->front));
}
Mat4 camera_get_view_matrix(Camera* camera) {
// Создаем матрицу вида из положения и направления камеры
Vec3 target = vec3_add(camera->position, camera->front);
return look_at(camera->position, target, camera->up);
}
Для реализации освещения создадим структуру света и шейдер:
typedef enum {
LIGHT_DIRECTIONAL,
LIGHT_POINT,
LIGHT_SPOT
} LightType;
typedef struct {
LightType type;
Vec3 position; // для точечных и прожекторных
Vec3 direction; // для направленных и прожекторных
Vec3 ambient;
Vec3 diffuse;
Vec3 specular;
// Параметры затухания для точечных и прожекторных
float constant;
float linear;
float quadratic;
// Для прожекторных
float cutOff;
float outerCutOff;
} Light;
Шейдер для освещения (фрагментный шейдер):
// fragment_shader.glsl
#version 330 core
out vec4 FragColor;
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;
uniform vec3 viewPos;
uniform sampler2D diffuseTexture;
struct Light {
int type; // 0 = directional, 1 = point, 2 = spot
vec3 position;
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
float cutOff;
float outerCutOff;
};
uniform Light light;
void main() {
// Текстурирование
vec3 color = texture(diffuseTexture, TexCoords).rgb;
// Ambient
vec3 ambient = light.ambient * color;
// Diffuse
vec3 norm = normalize(Normal);
vec3 lightDir;
if(light.type == 0) // directional
lightDir = normalize(-light.direction);
else
lightDir = normalize(light.position – FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * color;
// Specular
vec3 viewDir = normalize(viewPos – FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = light.specular * spec;
// Attenuation for point and spot lights
float attenuation = 1.0;
if(light.type != 0) { // not directional
float distance = length(light.position – FragPos);
attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
}
// Spotlight intensity
float intensity = 1.0;
if(light.type == 2) { // spotlight
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff – light.outerCutOff;
intensity = clamp((theta – light.outerCutOff) / epsilon, 0.0, 1.0);
}
// Combine results
diffuse *= attenuation * intensity;
specular *= attenuation * intensity;
vec3 result = ambient + diffuse + specular;
FragColor = vec4(result, 1.0);
}
Для работы с текстурами создадим соответствующие функции:
typedef struct {
unsigned int id;
int width, height, channels;
} Texture;
Texture* texture_load(const char* path) {
Texture* texture = malloc(sizeof(Texture));
// Загружаем изображение
unsigned char* data = stbi_load(path,
&texture->width,
&texture->height,
&texture->channels,
0);
if (!data) {
fprintf(stderr, "Failed to load texture: %s\n", path);
free(texture);
return NULL;
}
// Создаем OpenGL текстуру
glGenTextures(1, &texture->id);
glBindTexture(GL_TEXTURE_2D, texture->id);
// Настраиваем параметры
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Загружаем данные и генерируем мипмапы
GLenum format = GL_RGB;
if (texture->channels == 1)
format = GL_RED;
else if (texture->channels == 3)
format = GL_RGB;
else if (texture->channels == 4)
format = GL_RGBA;
glTexImage2D(GL_TEXTURE_2D, 0, format,
texture->width, texture->height,
0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
// Освобождаем данные изображения
stbi_image_free(data);
return texture;
}
Оптимизация производительности 3D движка в проектах
Оптимизация — критически важный аспект создания 3D движка. Даже простой движок может быть чрезвычайно требовательным к ресурсам, если не оптимизирован должным образом. 🚀
Ключевые стратегии оптимизации:
- Отсечение по видимости — рендеринг только тех объектов, которые видны на экране
- Уровни детализации (LOD) — уменьшение детализации удаленных объектов
- Пакетная обработка — группировка аналогичных объектов для уменьшения вызовов отрисовки
- Оптимизация шейдеров — упрощение шейдеров для повышения производительности
- Управление памятью — эффективная загрузка и выгрузка ресурсов
Реализуем функцию отсечения по видимости (Frustum Culling):
typedef struct {
Vec4 planes[6]; // left, right, bottom, top, near, far
} Frustum;
// Извлечение отсекающих плоскостей из матрицы проекции и вида
void frustum_extract(Frustum* frustum, Mat4 view_proj) {
// Левая плоскость
frustum->planes[0].x = view_proj.m[0][3] + view_proj.m[0][0];
frustum->planes[0].y = view_proj.m[1][3] + view_proj.m[1][0];
frustum->planes[0].z = view_proj.m[2][3] + view_proj.m[2][0];
frustum->planes[0].w = view_proj.m[3][3] + view_proj.m[3][0];
// Правая плоскость
frustum->planes[1].x = view_proj.m[0][3] – view_proj.m[0][0];
frustum->planes[1].y = view_proj.m[1][3] – view_proj.m[1][0];
frustum->planes[1].z = view_proj.m[2][3] – view_proj.m[2][0];
frustum->planes[1].w = view_proj.m[3][3] – view_proj.m[3][0];
// Аналогично для других плоскостей...
// Нормализация плоскостей
for(int i = 0; i < 6; i++) {
float length = sqrt(frustum->planes[i].x * frustum->planes[i].x +
frustum->planes[i].y * frustum->planes[i].y +
frustum->planes[i].z * frustum->planes[i].z);
frustum->planes[i].x /= length;
frustum->planes[i].y /= length;
frustum->planes[i].z /= length;
frustum->planes[i].w /= length;
}
}
// Проверка, видна ли сфера в отсекающих плоскостях
bool frustum_sphere_visible(Frustum* frustum, Vec3 center, float radius) {
for(int i = 0; i < 6; i++) {
// Расстояние от центра сферы до плоскости
float distance = frustum->planes[i].x * center.x +
frustum->planes[i].y * center.y +
frustum->planes[i].z * center.z +
frustum->planes[i].w;
// Если сфера полностью за плоскостью, она невидима
if(distance < -radius)
return false;
}
// Сфера видима или пересекается с отсекающими плоскостями
return true;
}
Реализация простой системы управления уровнями детализации:
typedef struct {
Mesh* high_detail; // Для близких расстояний
Mesh* medium_detail; // Для средних расстояний
Mesh* low_detail; // Для дальних расстояний
float medium_distance;
float low_distance;
} LODModel;
// Выбор подходящего меша в зависимости от расстояния до камеры
Mesh* lod_select_mesh(LODModel* model, Vec3 position, Camera* camera) {
float distance = vec3_distance(position, camera->position);
if(distance < model->medium_distance)
return model->high_detail;
else if(distance < model->low_distance)
return model->medium_detail;
else
return model->low_detail;
}
| Техника оптимизации | Влияние на производительность | Сложность реализации | Применимость |
|---|---|---|---|
| Frustum Culling | Высокое | Средняя | Для всех сцен с множеством объектов |
| Level of Detail (LOD) | Высокое | Средняя | Для сложных моделей и открытых пространств |
| Occlusion Culling | Высокое | Высокая | Для сцен с множеством перекрывающихся объектов |
| Instancing | Среднее | Низкая | Для сцен с повторяющимися объектами |
| Texture Atlasing | Среднее | Низкая | Для оптимизации использования памяти GPU |
| Shader Optimization | Среднее-Высокое | Высокая | Для всех проектов |
Для оптимизации рендеринга похожих объектов используйте инстансинг:
// Подготовка данных для инстансинга
void prepare_instanced_rendering(Mesh* mesh, int instance_count, Mat4* model_matrices) {
// Создаем буфер для матриц моделей
unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, instance_count * sizeof(Mat4),
&model_matrices[0], GL_STATIC_DRAW);
// Настраиваем атрибуты инстансинга для матриц модели
glBindVertexArray(mesh->VAO);
// mat4 = 4 vec4, поэтому занимаем 4 атрибута
for(int i = 0; i < 4; i++) {
glEnableVertexAttribArray(3 + i);
glVertexAttribPointer(3 + i, 4, GL_FLOAT, GL_FALSE,
sizeof(Mat4), (void*)(i * sizeof(Vec4)));
glVertexAttribDivisor(3 + i, 1); // Изменяем для каждого инстанса
}
glBindVertexArray(0);
}
// Рендеринг инстансированных объектов
void render_instanced(Mesh* mesh, Shader* shader,
Texture* texture, int instance_count) {
glUseProgram(shader->id);
// Привязываем текстуру
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture->id);
glUniform1i(glGetUniformLocation(shader->id, "diffuseTexture"), 0);
// Установка других параметров шейдера...
// Рендеринг инстансов
glBindVertexArray(mesh->VAO);
glDrawElementsInstanced(GL_TRIANGLES, mesh->index_count,
GL_UNSIGNED_INT, 0, instance_count);
glBindVertexArray(0);
}
Создание 3D движка на C — сложный, но невероятно вознаграждающий опыт. Овладев основами, представленными в этом руководстве, вы заложили фундамент для разработки более сложных систем рендеринга и графических приложений. Помните, что практика и экспериментирование — лучший способ углубить понимание компьютерной графики. Не бойтесь исследовать, модифицировать и расширять базовый движок, добавляя новые возможности и оптимизации.
Читайте также
- Математика в 3D графике: превращаем формулы в инструменты творчества
- Освещение и тени в 3D графике на C: руководство разработчика
- Матрицы трансформации в 3D: ключи к управлению виртуальным миром
- ANGLE: мост между OpenGL ES и нативными графическими API
- Трехмерное вращение объектов: математика, техники, решения
- Матрица масштабирования в 3D: создание и трансформация объектов
- Матрицы преобразований в 3D-графике: ключ к управлению объектами
- Матрицы поворота в 3D-графике: от теории к реальным проектам
- 15 библиотек для 3D-графики на C: мощные инструменты разработки
- Освоение 3D-программирования на C: от основ до создания игр