Oh, and the documentation: It's extensive but never tells me what I need to know.

Эта цитата взята из обсуждения CMake на Reddit, и она очень точно описывает большую часть моих проблем с CMake: когда я хочу что-то сделать, документация не помогает с этим вообще, - приходится искать решения в чужих проектах и статьях.

В этой статье будут разобраны проблемы, с которыми я столкнулся в процессе изучения Vulkan. Однако материал будет полезен и тем, кто настраивает любой другой проект.

Ошибки?

Если вы нашли ошибки, неточности или можете предложить лучшее решение, то добро пожаловать в комментарии - я исправлю или дополню статью.

Оглавление

Подключение библиотек через FetchContent

Много моих товарищей, которые только начинали изучение C++, тратили ужасно большое количество времени каждый раз, когда надо было установить библиотеку. Здесь я покажу только один из способов - CMake + FetchContent, однако есть и другие: vcpkg, скачивание вручную, github modules...

Стандартная последовательность действий:

  1. Включить модуль FetchContent.

include(FetchContent)
  1. Указать репозиторий, откуда будет загружаться библиотека, и дать этому контенту имя.

FetchContent_Declare(
  <name>
  GIT_REPOSITORY <url>
  GIT_TAG <tag>
)

<tag> может быть как хэшем коммита, так и тэгом.

  1. Загрузить и интегрировать библиотеку (добавить цели) в проект.

FetchContent_MakeAvailable(<name>)
  1. Связать библиотеку с нужной целью.

target_link_libraries(<your_target> <library_target>)

<library_target> - это цель, которую создала библиотека. Её можно найти в примерах использования библиотеки, в README.md (там вообще можно много найти) или в CMakeLists.txt. В последнем случае надо искать строки вида add_library(<library_target> ...).

Пример
include(FetchContent)

FetchContent_Declare(
        glfw
        GIT_REPOSITORY https://github.com/glfw/glfw.git
        GIT_TAG 3.4
)

FetchContent_MakeAvailable(
        glfw
)

target_link_libraries(engine PRIVATE glfw)

Такая последовательность действует в большинстве случаев, но не во всех.

Библиотеки без CMakeLists.txt

Если у библиотеки нет CMakeLists.txt, то можно её собрать самостоятельно. Нужно написать такой же код, который нужен для сборки вашего проекта с некоторыми нюансами.

Мы как и с другими библиотеками после использование FetchContent_MakeAvailable создаём цель.

add_library(imgui_l STATIC)

Однако в нашем проекте мы обычно используем CMAKE_CURRENT_SOURCE_DIR, чтобы включить директории с файлами, но при использовании FetchContent, файлы библиотеки лежат где-то в папке сборки, а потому не хочется создавать файлы там.

Решить эту проблему помогает то, что FetchContent_MakeAvailable, создаёт переменную вида <name>_SOURCE_DIR для каждого имени из FetchContent_Declare, которая содержит путь до скачанного репозитория. Поэтому мы можем использовать её для того, чтобы указать нужные файлы.

target_include_directories(imgui_l PRIVATE ${imgui_SOURCE_DIR})

target_sources(imgui_l PRIVATE
        ${imgui_SOURCE_DIR}/imgui.h
        ${imgui_SOURCE_DIR}/imgui.cpp

        ${imgui_SOURCE_DIR}/imgui_demo.cpp
        ${imgui_SOURCE_DIR}/imgui_draw.cpp
        ${imgui_SOURCE_DIR}/imgui_widgets.cpp

        ${imgui_SOURCE_DIR}/backends/imgui_impl_vulkan.cpp
)

А также некоторые библиотеки используют внутри другие библиотеки, поэтому их тоже нужно подключить.

target_link_libraries(imgui_l PRIVATE Vulkan::Vulkan)

И в конце как и остальные библиотеки нужно связать с целью.

target_link_libraries(engine PRIVATE imgui_l)

Однако иногда очень сложно написать самому такой скрипт, так как предполагалась сборка библиотеки с помощью других инструментов.

Скомпилированные библиотеки

Нередко разработчики предоставляют файлы .a (используется в Unix-подобных системах (например, Linux, macOS) и компиляторами, такими как GCC ) или .lib (используется в Windows и компиляторами, такими как MSVC ) - статические библиотеки.

Их обычно скачивают и кладут в отдельную папку lib или external в корне проекта, однако помимо самих файлов библиотеки ещё нужно положить куда-то заголовки этой библиотеки (обычно папка include).

Соответственно в самом CMake нужно сделать:

target_link_libraries(engine PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/lib/lib-mingw-w64/libglfw3.a)
# Or
target_link_libraries(engine PRIVATE
  ${CMAKE_CURRENT_SOURCE_DIR}/lib/lib-vc2022/glfw3.lib)

target_include_directories(engine PRIVATE 
  ${CMAKE_CURRENT_SOURCE_DIR}/include)
Полный пример

Здесь используется способ загрузки, описанный в Ускорение загрузки библиотек.

При использовании иных операционных систем или компиляторов нужно заменить ссылку на архив и путь до файла библиотеки.

FetchContent_Declare(
        glfw
        URL https://github.com/glfw/glfw/releases/download/3.4/glfw-3.4.bin.WIN64.zip
)

FetchContent_MakeAvailable(
        glfw
)

target_link_libraries(engine PRIVATE ${glfw_SOURCE_DIR}/lib-mingw-w64/libglfw3.a)
target_include_directories(engine PRIVATE
        ${glfw_SOURCE_DIR}/include)

Такой способ имеет свои минусы: мы не можем настроить определения или флаги компиляции для конкретно этой библиотеки. Поэтому будет правильней добавить отдельную цель для этой библиотеки (этот способ я взял из комментариев к статье).

add_library(glfw STATIC IMPORTED)

Параметр IMPORTED указывает на то, что библиотека уже скомпилирована.

Для этой цели можно также указать target_link_libraries и target_include_directories, но с модификатором PUBLIC, так как нам нужно прокинуть эти заголовки в места, где используется эта библиотека. Однако более корректным будет использовать свойства.

set_target_properties(glfw PROPERTIES
  IMPORTED_LOCATION "${glfw_SOURCE_DIR}/lib-mingw-w64/libglfw3.a"
  INTERFACE_INCLUDE_DIRECTORIES "${glfw_SOURCE_DIR}/include"
)

Остаётся как и во всех случаях только подключить эту библиотеку.

target_link_libraries(engine PRIVATE glfw)

Header-only библиотеки

Ещё один возможный вариант - репозиторий только с заголовками. Однако ничего сложного тут нет: единственное, что нужно - сделать директорию с ними доступной в проекте после использования FetchContent_MakeAvailable.

target_include_directories(engine PRIVATE ${stb_SOURCE_DIR})

Или как и в предыдущем случае создать библиотеку, но в отличие от импортированной нам нужно использовать параметр INTERFACE, так как библиотека состоит из заголовков и её не нужно компилировать.

add_library(stb INTERFACE)
set_target_properties(stb PROPERTIES
        INTERFACE_INCLUDE_DIRECTORIES ${stb_SOURCE_DIR}
)

Ускорение загрузки библиотек

Так как описанный изначально способ скачивания библиотек скачивает не только нужный вам коммит, а всю историю репозитория, из-за чего первый раз проект CMake может очень долго загружаться. Посмотреть, что происходит при загрузке библиотеки сначала нужно отключить FETCHCONTENT_QUIET (по умолчанию стоит ON), что перестанет подавлять вывод информации. А после уже в FetchContent_Declare указать, что мы хотим получать информацию о прогрессе.

set(FETCHCONTENT_QUIET OFF)
FetchContent_Declare(
        json
        GIT_REPOSITORY https://github.com/nlohmann/json
        GIT_TAG v3.11.3
        GIT_PROGRESS ON
)
Receiving objects: 100% (44890/44890), 193.92 MiB | 1.32 MiB/s, done.

Понятное дело, что нам не нужны все 200 мегабайт, а потому нам хочется скачивать только конкретный коммит. В документации можно найти GIT_SHALLOW, который должен скачивать только нужный коммит, но по факту он скачивает намного больше, а потому не очень помогает.

Receiving objects: 100% (8981/8981), 161.64 MiB | 3.79 MiB/s, done.

Поэтому есть другое решение: скачивать архив по конкретной ссылке.

FetchContent_Declare(
        json
        URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz
)

Чтобы найти нужную ссылку, мы заходим на страницу репозитория, в раздел релизов и копируем ссылку на файл с исходниками. Так мы скачаем только нужный коммит. Остальная часть алгоритма будет прежней (FetchContent_MakeAvailable и target_link_libraries()).

Подключение Vulkan

Так как Vulkan - это полноценный SDK (набор инструментов разработки), то он содержит в себе библиотеки, заголовки, документацию, примеры и инструменты. Поэтому его приходится устанавливать отдельно как программу. Стандартная последовательность действий такова:

  • Скачайте Vulkan SDK с официального сайта LunarG Vulkan SDK.

  • Установить его.

  • Найти и подключить в CMake.

find_package(Vulkan REQUIRED)
target_link_libraries(engine PRIVATE Vulkan::Vulkan)

Однако CMake может не найти этот пакет, так как что-то произошло с переменной окружения VULKAN_SDK, которая указывает путь до установленного Vulkan. Эта переменная автоматически должна быть создана при установке, поэтому если она отсутствует, то её надо создать.

Проблемы с CLion

Лично я столкнулся с немного иной проблемой: в системных переменных окружения VULKAN_SDK была, но CLion её не подгрузил и в Settings -> Build, Execution, Deployment -> CMake в используемом профиле Environment её не было (надо нажать на кнопку странички, чтобы просмотреть переменные). Позже CLion её подгрузит, но на время можно самостоятельно добавить её.

VULKAN_SDK=<path_to_vulkan>

Настройка и компиляция исходников

Автоматическое добавление исходников к цели

Часто в проекте все файлы должны быть скомпилированы, поэтому нет смысла в перечислении их вручную. И чтобы упростить себе задачу и не заниматься этим, можно использовать команду file вместе с GLOB_RECURSE.

file(GLOB_RECURSE CPP_SOURCE_FILES CONFIGURE_DEPENDS
        "${PROJECT_SOURCE_DIR}/src/*.cpp"
)

GLOB_RECURSE собирает в список все файлы, соответствующие предоставленному выражению: в нашем случае все .cpp файлы. Стоит отметить, что GLOB_RECURSE рекурсивно проверяет все директории, в то время как просто GLOB этого не делает.

Ещё одна важная деталь - это CONFIGURE_DEPENDS. Этот параметр пересобирает проект CMake, если значение переменной (CPP_SOURCE_FILES) должно измениться.

Однако также стоит обратить внимание на критику GLOB: в документации предупреждают, что CONFIGURE_DEPENDS работает надёжно не на всех генераторах, а также отмечают, что эта проверка всё равно требует время при каждой сборке. Для средних и больших проектов эти факторы могут играть большую роль, из-за чего в интернете часто появляется вопрос "Why is cmake file GLOB evil?", но об этом способе стоит упомянуть, так как он сильно облегчает жизнь на первых порах.

После записи всех файлов в переменную, нужно как обычно добавить исходники к цели.

target_include_directories(engine PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")

Добавление define во все исходники

Такие библиотеки как GLFW и GLM требуют перед каждым включением заголовка прописывать директиву define для настройки каких-либо параметров, которые по факту едины для всего проекта.

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>

Часто для решение этой проблемы создают дополнительный заголовочный файл с таким кодом и включают везде именно его. Однако в CMake существует возможность задать эти определения для цели.

target_compile_definitions(engine PRIVATE
        GLM_FORCE_DEPTH_ZERO_TO_ONE
        GLM_FORCE_RADIANS

        GLFW_INCLUDE_VULKAN
)

Предварительная компиляция заголовков

Хороший способ сократить время компиляции - это выделить все заголовки, которые практически не меняются (часто берут именно из библиотек) и часто используются, и сделать для них предкомпиляцию.

Суть в том, что из-за включения заголовка в множество .cpp файлов, при компиляции этих файлов мы также раз за разом компилируем один и тот же заголовок. Поэтому можно сказать компилятору, какие файлы можно предкомпилировать и не тратить на них время во время компиляции.

target_precompile_headers(engine PRIVATE
        <optional>
        <memory>
        <string>
        <vector>
        <unordered_map>
        <glm/mat4x4.hpp>
        <glm/vec4.hpp>
        <vulkan/vulkan.h>
)

На просторах интернета нашёл вот такой небольшой список, подходящий моему проекту.

Компиляция шейдеров

Чтобы всё нужное в проекте собиралось по нажатию одной кнопки, не хватает сделать компиляцию шейдеров также автоматической. Аналогично добавлению исходников к цели можно собрать все файлы шейдеров и уже над ними производить манипуляции.

file(GLOB_RECURSE GLSL_SOURCE_FILES CONFIGURE_DEPENDS
        "${PROJECT_SOURCE_DIR}/shaders/*.frag"
        "${PROJECT_SOURCE_DIR}/shaders/*.vert"
        "${PROJECT_SOURCE_DIR}/shaders/*.comp"
)

По списоку мы можем проитерироваться, скомпилировав каждый файл по отдельности.

foreach (GLSL IN LISTS GLSL_SOURCE_FILES)
  # ...
endforeach (GLSL)

Так как мы хотим сохранить структуру директорий и названия в папке с скомпилированными шейдерами, то просто возьмём относительный путь от папки с шейдерами до файла и добавим расширение .spv для файла.

file(RELATIVE_PATH FILE_NAME "${PROJECT_SOURCE_DIR}/shaders/" "${GLSL}")
set(SPIRV "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/compiled_shaders/${FILE_NAME}.spv")

Далее нужно определить команду, которая создаст наш файл.

add_custom_command(
            OUTPUT ${SPIRV}
            COMMAND Vulkan::glslc ${GLSL} -o ${SPIRV}
            DEPENDS ${GLSL})

DEPENDS - файл, при изменении которого будет исполняться команда.

COMMAND - сама команда, которую мы фактически можем вставить в консоль с параметрами, использующимися при запуске этой команды. В данном случае CMake сам подставит вместо Vulkan::glslc путь к исполняемому файлу.

glslc - это компонент (программа), который был найден в процессе выполнения команды find_package(Vulkan REQUIRED), а конкретнее инструмент компиляции и оптимизации шейдеров. Однако этот инструмент - только обёртка для glslangValidator (компилятор) и spirv-opt (оптимизатор), которые также можно использовать.

OUTPUT - файл, который будет получен в результате выполнения команды. Этот параметр указывается, чтобы CMake автоматически мог построить зависимости между командой и целью из той же области видимости. Сама по себе команда не будет выполняться, так как не прикреплена к цели, поэтому важно создать зависимость какой-либо цели от сгенерированных файлов. Для этого мы соберём их в список и создадим цель, зависящую от этих файлов (параметр DEPENDS) и которая будет частью стандартной сборки (параметр ALL).

foreach (GLSL IN LISTS GLSL_SOURCE_FILES)
    file(RELATIVE_PATH FILE_NAME "${PROJECT_SOURCE_DIR}/shaders/" "${GLSL}")
    set(SPIRV "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/compiled_shaders/${FILE_NAME}.spv")

    add_custom_command(
            OUTPUT ${SPIRV}
            COMMAND Vulkan::glslc ${GLSL} -o ${SPIRV}
            DEPENDS ${GLSL})

    list(APPEND SPIRV_BINARY_FILES ${SPIRV})
endforeach (GLSL)

add_custom_target(
        ShadersTarget ALL
        DEPENDS ${SPIRV_BINARY_FILES}
)

Заключение

Я потратил довольно много времени на настройку, а не на программирование, что меня немного удручает, однако в будущем с этими знаниями я смогу намного быстрее приступить к непосредственной разработке. А потому в дополнение хочу рассказать ещё об одной вещи, на которую я потратил очень много времени.

Многие слышали фразу "исключения очень медленные". Но также многие (как я раньше или мои товарищи) неправильно её понимают: нам кажется, что простое наличие исключений в коде делает его значительно медленней (ведь там генерируется какая-то магия для их обработки), однако сейчас используется модель zero-cost exceptions, которая позволяет избавиться от накладных затрат в коде, который не выбрасывает исключения. Из этого следует, что пытаться отключить полностью их с помощью флагов компилятора практически бессмысленно (-fno-exceptions). Однако кинуть исключение всё равно очень дорого, а значит делать это стоит только в исключительных ситуациях (а-ля не может инициализироваться какая-то критически важная библиотека), в остальных ситуациях нужно обходиться кодами возврата или Result Type.

Надеюсь кому-то эта статья поможет. Спасибо, что дочитали.

Комментарии (23)


  1. uxgen
    15.01.2025 09:51

    Веселее настроить cmake под android, там всякие fetch content могут не работать.


    1. Jijiki
      15.01.2025 09:51

      на сколько помню в андроид студио запускается Симейк сборка или .mk (это в сдл2-3 по крайней мере)


    1. Tujh
      15.01.2025 09:51

      Они начали активно поддерживать и обновлять версию CMake в SDK в последнее время, но на сколько хорошо это работает - не проверял.


  1. voldemar_d
    15.01.2025 09:51

    <library_target> - это цель, которую создала библиотека. Её можно найти в примерах использования библиотеки, в README.md (там вообще можно много найти) или в CMakeLists.txt. В последнем случае надо искать строки вида add_library(<library_target> ...).

    Есть ли где-нибудь набор полезных советов на эту тему?

    Не раз уже сталкивался с ситуациями, что то, что надо указать в качестве library_target, почему-то не соответствует тому, что приводится в readme (возможно, там информация просто устаревшая), и приходится гуглить, как именно подключить библиотеку в CMakeLists.txt. И даже это не всегда помогает - зачастую находятся советы и примеры использования только 10-летней давности, которые уже не работают.

    Причем, бывает, что под Linux всё собирается, а под Windows в MSVC нет.


    1. Jijiki
      15.01.2025 09:51

      Скрытый текст

      главный симейк собирающий таргеты

      Скрытый текст
      cmake_minimum_required(VERSION 3.5)
      set (CMAKE_CXX_STANDARD 23)
      
      project(opengl4-tutorial) # "Solution" name
      #add_subdirectory(third_party)
      add_library(glm INTERFACE)
      
      add_library(stb_truetype STATIC)
      
      add_library(stb_image_write STATIC)
      
      add_library(stb_image STATIC)
      
      add_library(WTMath STATIC)
      
      add_library(lua STATIC)
      
      add_library(sqlite3 STATIC)
      
      # target_include_directories(sqlite3 INTERFACE sqlite3)
      
      target_include_directories(lua INTERFACE lua)
      
      target_include_directories(glm INTERFACE glm)
      
      target_include_directories(WTMath INTERFACE WTMath)
      
      target_sources(sqlite3 PRIVATE 
          third_party/sqlite3/sqlite3.h
          third_party/sqlite3/sqlite3.c
          third_party/sqlite3/sqlite3ext.h
          )
      target_include_directories(sqlite3 PUBLIC sqlite3)
      
      target_sources(stb_image PRIVATE 
          third_party/stb_image/stb_image.h
          third_party/stb_image/stb_image.cc
          )
      target_include_directories(stb_image PUBLIC stb_image)
      
      target_sources(stb_image_write PRIVATE 
          third_party/stb_image_write/stb_image_write.h
          third_party/stb_image_write/stb_image_write.cc
          )
      target_include_directories(stb_image_write PUBLIC stb_image_write)
      
      target_sources(stb_truetype PRIVATE 
          third_party/stb_truetype/stb_truetype.h
          third_party/stb_truetype/stb_truetype.cc
          )
      target_include_directories(stb_truetype PUBLIC stb_truetype)
      
      target_sources(WTMath PRIVATE 
          third_party/WTMath/WTMath.h
          third_party/WTMath/WTMath.c
          )
      target_include_directories(WTMath PUBLIC WTMath)
      
      target_sources(lua PRIVATE
      third_party/lua/src/lapi.c
      third_party/lua/src/lapi.h
      third_party/lua/src/lauxlib.c
      third_party/lua/src/lauxlib.h
      third_party/lua/src/lbaselib.c
      third_party/lua/src/lcode.c
      third_party/lua/src/lcode.h
      third_party/lua/src/lcorolib.c
      third_party/lua/src/lctype.c
      third_party/lua/src/lctype.h
      third_party/lua/src/ldblib.c
      third_party/lua/src/ldebug.c
      third_party/lua/src/ldebug.h
      third_party/lua/src/ldo.c
      third_party/lua/src/ldo.h
      third_party/lua/src/ldump.c
      third_party/lua/src/lfunc.c
      third_party/lua/src/lfunc.h
      third_party/lua/src/lgc.c
      third_party/lua/src/lgc.h
      third_party/lua/src/linit.c
      third_party/lua/src/liolib.c
      third_party/lua/src/ljumptab.h
      third_party/lua/src/llex.c
      third_party/lua/src/llex.h
      third_party/lua/src/llimits.h
      third_party/lua/src/lmathlib.c
      third_party/lua/src/lmem.c
      third_party/lua/src/lmem.h
      third_party/lua/src/loadlib.c
      third_party/lua/src/lobject.c
      third_party/lua/src/lobject.h
      third_party/lua/src/lopcodes.c
      third_party/lua/src/lopcodes.h
      third_party/lua/src/lopnames.h
      third_party/lua/src/loslib.c
      third_party/lua/src/lparser.c
      third_party/lua/src/lparser.h
      third_party/lua/src/lprefix.h
      third_party/lua/src/lstate.c
      third_party/lua/src/lstate.h
      third_party/lua/src/lstring.c
      third_party/lua/src/lstring.h
      third_party/lua/src/lstrlib.c
      third_party/lua/src/ltable.c
      third_party/lua/src/ltable.h
      third_party/lua/src/ltablib.c
      third_party/lua/src/ltm.c
      third_party/lua/src/ltm.h
      third_party/lua/src/lua.c
      third_party/lua/src/luac.c
      third_party/lua/src/luaconf.h
      third_party/lua/src/lua.h
      third_party/lua/src/lua.hpp
      third_party/lua/src/lualib.h
      third_party/lua/src/lundump.c
      third_party/lua/src/lundump.h
      third_party/lua/src/lutf8lib.c
      third_party/lua/src/lvm.c
      third_party/lua/src/lvm.h
      third_party/lua/src/lzio.c
      third_party/lua/src/lzio.h
      # Makefile
          
      )
      target_include_directories(lua PUBLIC lua)
      
      # assimp
      set(ASSIMP_DIR "third_party/assimp")
      
      set(BUILD_SHARED_LIBS OFF)
      set(ASSIMP_BUILD_TESTS OFF CACHE INTERNAL "Build the ASSIMP test programs")
      set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE INTERNAL "Build the ASSIMP tools")
      set(ASSIMP_NO_EXPORT OFF CACHE INTERNAL "Disable ASSIMP Export")
      set(ASSIMP_BUILD_COLLADA_IMPORTER ON)
      set(ASSIMP_BUILD_FBX_IMPORTER ON)
      # set(ASSIMP_BUILD_COLLADA_EXPORTER ON)
      # set(ASSIMP_BUILD_FBX_EXPORTER ON)
      set(ASSIMP_BUILD_AMF_IMPORTER OFF)
      set(ASSIMP_BUILD_3DS_IMPORTER ON)
      set(ASSIMP_BUILD_AC_IMPORTER OFF)
      set(ASSIMP_BUILD_ASE_IMPORTER OFF)
      set(ASSIMP_BUILD_ASSBIN_IMPORTER OFF)
      set(ASSIMP_BUILD_B3D_IMPORTER OFF)
      set(ASSIMP_BUILD_BVH_IMPORTER OFF)
      set(ASSIMP_BUILD_DXF_IMPORTER OFF)
      set(ASSIMP_BUILD_CSM_IMPORTER OFF)
      set(ASSIMP_BUILD_HMP_IMPORTER OFF)
      set(ASSIMP_BUILD_IRRMESH_IMPORTER OFF)
      set(ASSIMP_BUILD_IRR_IMPORTER OFF)
      set(ASSIMP_BUILD_LWO_IMPORTER OFF)
      set(ASSIMP_BUILD_LWS_IMPORTER OFF)
      set(ASSIMP_BUILD_MD2_IMPORTER OFF)
      set(ASSIMP_BUILD_MD3_IMPORTER OFF)
      set(ASSIMP_BUILD_MD5_IMPORTER OFF)
      set(ASSIMP_BUILD_MDC_IMPORTER OFF)
      set(ASSIMP_BUILD_MDL_IMPORTER OFF)
      set(ASSIMP_BUILD_NFF_IMPORTER OFF)
      set(ASSIMP_BUILD_NDO_IMPORTER OFF)
      set(ASSIMP_BUILD_OFF_IMPORTER OFF)
      set(ASSIMP_BUILD_OBJ_IMPORTER ON)
      # set(ASSIMP_BUILD_OBJ_EXPORTER ON)
      set(ASSIMP_BUILD_OGRE_IMPORTER OFF)
      set(ASSIMP_BUILD_OPENGEX_IMPORTER OFF)
      set(ASSIMP_BUILD_PLY_IMPORTER OFF)
      set(ASSIMP_BUILD_MS3D_IMPORTER OFF)
      set(ASSIMP_BUILD_COB_IMPORTER OFF)
      set(ASSIMP_BUILD_BLEND_IMPORTER OFF)
      set(ASSIMP_BUILD_IFC_IMPORTER OFF)
      set(ASSIMP_BUILD_XGL_IMPORTER OFF)
      set(ASSIMP_BUILD_Q3D_IMPORTER OFF)
      set(ASSIMP_BUILD_Q3BSP_IMPORTER OFF)
      set(ASSIMP_BUILD_RAW_IMPORTER OFF)
      set(ASSIMP_BUILD_SIB_IMPORTER OFF)
      set(ASSIMP_BUILD_SMD_IMPORTER OFF)
      set(ASSIMP_BUILD_STL_IMPORTER OFF)
      set(ASSIMP_BUILD_TERRAGEN_IMPORTER OFF)
      set(ASSIMP_BUILD_3D_IMPORTER OFF)
      set(ASSIMP_BUILD_X_IMPORTER OFF)
      set(ASSIMP_BUILD_X3D_IMPORTER OFF)
      set(ASSIMP_BUILD_GLTF_IMPORTER OFF)
      set(ASSIMP_BUILD_3MF_IMPORTER OFF)
      set(ASSIMP_BUILD_MMD_IMPORTER OFF)
      set(ASSIMP_BUILD_STEP_IMPORTER OFF)
      set(ASSIMP_BUILD_COLLADA_IMPORTER ON)
      set(ASSIMP_BUILD_FBX_IMPORTER ON)
      # set(ASSIMP_BUILD_COLLADA_EXPORTER ON)
      # set(ASSIMP_BUILD_FBX_EXPORTER ON)
      set(ASSIMP_BUILD_AMF_IMPORTER OFF)
      set(ASSIMP_BUILD_3DS_EXPORTER OFF)
      set(ASSIMP_BUILD_AC_EXPORTER OFF)
      set(ASSIMP_BUILD_ASE_EXPORTER OFF)
      set(ASSIMP_BUILD_ASSBIN_EXPORTER OFF)
      set(ASSIMP_BUILD_B3D_EXPORTER OFF)
      set(ASSIMP_BUILD_BVH_EXPORTER OFF)
      set(ASSIMP_BUILD_DXF_EXPORTER OFF)
      set(ASSIMP_BUILD_CSM_EXPORTER OFF)
      set(ASSIMP_BUILD_HMP_EXPORTER OFF)
      set(ASSIMP_BUILD_IRRMESH_EXPORTER OFF)
      set(ASSIMP_BUILD_IRR_EXPORTER OFF)
      set(ASSIMP_BUILD_LWO_EXPORTER OFF)
      set(ASSIMP_BUILD_LWS_EXPORTER OFF)
      set(ASSIMP_BUILD_MD2_EXPORTER OFF)
      set(ASSIMP_BUILD_MD3_EXPORTER OFF)
      set(ASSIMP_BUILD_MD5_EXPORTER OFF)
      set(ASSIMP_BUILD_MDC_EXPORTER OFF)
      set(ASSIMP_BUILD_MDL_EXPORTER OFF)
      set(ASSIMP_BUILD_NFF_EXPORTER OFF)
      set(ASSIMP_BUILD_NDO_EXPORTER OFF)
      set(ASSIMP_BUILD_OFF_EXPORTER OFF)
      set(ASSIMP_BUILD_OBJ_EXPORTER ON)
      # set(ASSIMP_BUILD_OBJ_EXPORTER ON)
      set(ASSIMP_BUILD_OGRE_EXPORTER OFF)
      set(ASSIMP_BUILD_OPENGEX_EXPORTER OFF)
      set(ASSIMP_BUILD_PLY_EXPORTER OFF)
      set(ASSIMP_BUILD_MS3D_EXPORTER OFF)
      set(ASSIMP_BUILD_COB_EXPORTER OFF)
      set(ASSIMP_BUILD_BLEND_EXPORTER OFF)
      set(ASSIMP_BUILD_IFC_EXPORTER OFF)
      set(ASSIMP_BUILD_XGL_EXPORTER OFF)
      set(ASSIMP_BUILD_Q3D_EXPORTER OFF)
      set(ASSIMP_BUILD_Q3BSP_EXPORTER OFF)
      set(ASSIMP_BUILD_RAW_EXPORTER OFF)
      set(ASSIMP_BUILD_SIB_EXPORTER OFF)
      set(ASSIMP_BUILD_SMD_EXPORTER OFF)
      set(ASSIMP_BUILD_STL_EXPORTER OFF)
      set(ASSIMP_BUILD_TERRAGEN_EXPORTER OFF)
      set(ASSIMP_BUILD_3D_EXPORTER OFF)
      set(ASSIMP_BUILD_X_EXPORTER OFF)
      set(ASSIMP_BUILD_X3D_EXPORTER OFF)
      set(ASSIMP_BUILD_GLTF_EXPORTER OFF)
      set(ASSIMP_BUILD_3MF_EXPORTER OFF)
      set(ASSIMP_BUILD_MMD_EXPORTER OFF)
      set(ASSIMP_BUILD_STEP_EXPORTER OFF)
      
      add_subdirectory(third_party/assimp)
      target_include_directories(assimp PUBLIC "${ASSIMP_DIR}/include")
      target_compile_definitions(assimp PUBLIC "ASSIMP_INCLUDE_NONE")
      
      set(SDL_TEST_LIBRARY OFF)
      set(SDL3MIXER_MIDI OFF)
      set(SDL_SHARE OFF)
      set(SDL_STATIC ON)
      # add_subdirectory(third_party/WTMath)
      add_subdirectory(third_party/SDL)
      add_subdirectory(third_party/SDL_image)
      add_subdirectory(third_party/SDL_ttf)
      add_subdirectory(third_party/SDL_mixer)
      add_subdirectory(third_party/SDL_net)
      add_subdirectory(third_party/freetype-2.13.3)
      # target_include_directories(freetype PUBLIC third_party/freetype-2.13.3/include)
      set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
      set (EXECUTABLE_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/bin")
      # Variables for common classes (used throughout all projects)
      set(COMMON_CLASSES_ROOT "${CMAKE_SOURCE_DIR}/common_classes")
      add_subdirectory(01-window)
      add_subdirectory(02-first-shader)
      add_subdirectory(03-first-color)
      add_subdirectory(04-entering-third-dimension)
      add_subdirectory(05-simple-camera)
      add_subdirectory(06-texture)
      add_subdirectory(07-MultiLayerTexture)
      add_subdirectory(08-torus-indexed)
      add_subdirectory(09-geometry-shader)
      add_subdirectory(10-assimp-model)
      message(STATUS "CXX Flags: " ${CMAKE_CXX_FLAGS})

      симейк примера 01

      Скрытый текст
      cmake_minimum_required(VERSION 3.27)
      
      set(PROJECT_NAME "01-window")
      project(${PROJECT_NAME}) # Project's name
      #cmake_minimum_required(VERSION 3.1) # Specify the minimum version for CMake
      set (CMAKE_CXX_STANDARD 23) # Support C++14
      
      file(GLOB TUTORIAL_001_SOURCE_FILES *.cpp)
      
      set(COMMON_CLASSES_SOURCE_FILES
      "${COMMON_CLASSES_ROOT}/OpenGLWindow.cpp"
      )
      
      set(COMMON_CLASSES_HEADER_FILES
      "${COMMON_CLASSES_ROOT}/OpenGLWindow.h"
      )
      
      add_compile_options(-std=c++23 -Ofast -funroll-all-loops)
      set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # does not produce the json file
      set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "") # works
      
      #set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
      #set (EXECUTABLE_OUTPUT_PATH "${PROJECT_SOURCE_DIR}/bin")
      set (CMAKE_CXX_STANDARD 23)
      
      # add_subdirectory(../third_party)
      
      include_directories(../third_party/stb_image)
      include_directories(../third_party/WTMath)
      include_directories(../third_party/stb_image_write)
      find_package(GLEW 2.0 REQUIRED)
      include_directories(${GLEW_INCLUDE_DIRS})
      add_executable(${PROJECT_NAME}
      ${TUTORIAL_001_SOURCE_FILES}
      ${COMMON_CLASSES_SOURCE_FILES}
      ${COMMON_CLASSES_HEADER_FILES}
      )
      
      add_definitions(-DGLEW_STATIC -DSDL_MAIN_HANDLED -DGLM_FORCE_DEPTH_ZERO_TO_ONE -DGLM_FORCE_RADIANS)
      
      target_link_libraries(${PROJECT_NAME} glm stb_image pthread dl m z WTMath GLEW::GLEW GL)
      
      target_include_directories(${PROJECT_NAME} PUBLIC ${GLEW_INCLUDE_DIRS})
      
      target_include_directories(${PROJECT_NAME} PUBLIC ../third_party/SDL/include)
      target_link_libraries(${PROJECT_NAME} SDL3-static)
      
      target_include_directories(${PROJECT_NAME} PUBLIC ../third_party/SDL_image/include)
      target_link_libraries(${PROJECT_NAME} SDL3_image-static)
      
      target_include_directories(${PROJECT_NAME} PUBLIC ../third_party/SDL_ttf/include)
      target_link_libraries(${PROJECT_NAME} SDL3_ttf-static)
      
      target_include_directories(${PROJECT_NAME} PUBLIC ../third_party/SDL_mixer/include)
      target_link_libraries(${PROJECT_NAME} SDL3_mixer-static)
      
      target_include_directories(${PROJECT_NAME} PUBLIC ../third_party/SDL_net/include)
      target_link_libraries(${PROJECT_NAME} SDL3_net-static)


      1. voldemar_d
        15.01.2025 09:51

        Вопрос у меня был, в основном, про эти два момента:

        target_include_directories(${PROJECT_NAME} PUBLIC ${GLEW_INCLUDE_DIRS})
        target_link_libraries(${PROJECT_NAME} SDL3-static)

        Как, в общем случае, догадаться при подключении сторонних библиотек, что нужно подставить в качестве ${GLEW_INCLUDE_DIRS} в target_include_directories и SDL3-static в target_link_libraries? Иногда приходится кучи вариантов чуть ли не наугад перебирать, если в README ничего не написано, либо что-то написано, но не работает.

        У каких-то библиотек есть файл .cmake, у каких-то нет, и вообще возможны самые разные варианты.


        1. Jijiki
          15.01.2025 09:51

          понял простите, еще надо контролировать какая сборка и присутствует в имени d при дебаге или нет и помойму имя под каждый тип сборки можно настроить, но сейчас не найду покачто, видел у себя тоже например есть моменты когда от типа сборки надо хендлить имя, а при другой конфигурации у вас всегда без d допустим одинаковое имя

          посмотрите на библиотеку WTMath(в тех двух скринах) я её написал и законфигурировал я вам кинул общий дизайн с настройками которые в ридми и когда вы сами написали либу

          (у ассимпа например есть файл симейк там есть переменные которые управляют сборкой, если проект аля исходник линкуете его в библиотеку наверно чтото такое)

          ${GLEW_INCLUDE_DIRS} - либо сами определяете по типу ${COMMON_INCLUDE_ROOT}, либо она у вас есть если есть то find найдёт ее в системе по файликам симейка как я понимаю


    1. Playa
      15.01.2025 09:51

      Совет 0: пользуйтесь пакетными менеджерами. Например, vcpkg зачастую подсказывает, как использовать библиотеку.
      Совет 1: ищите библиотеки через find_package. В приоритете использование cmake-конфига. Если его нет, можно попробовать встроенные в cmake find-модули. Если их нет можно попробовать pkg_check_modules (на windows тоже работает, но нужно где-то раздобыть pkgconf). Если вы, как и автор, пользуетесь FetchContent, подгоняйте своё использование под официальное (в принципе, в доках к FetchContent об этом пишут).
      Совет 2: пользуйтесь экспортированными таргетами. Имена таргетов обычно указывают в документации или в примерах использования, в худшем случае можно посмотреть в cmake-конфиге.
      Совет 3: пользуйтесь namespaced-таргетами.


      1. voldemar_d
        15.01.2025 09:51

        Спасибо. Про совет 3 можно подробнее - что имеется ввиду?


        1. Playa
          15.01.2025 09:51

          Некоторые библиотеки экспортируют таргеты без namespace и с namespace. Вроде, libpng раньше экспортировал png и PNG::PNG. При использовании первого о возможной ошибке вы узнаете только во время компиляции, а при использовании второго - ещё на этапе конфигурации.


          1. voldemar_d
            15.01.2025 09:51

            Понял. Собственно, после установки libpng с помощью vcpkg имеется файл usage:

            installed\x64-windows-static\share\libpng\usage

            И в нем написано, как подшивать библиотеку в CMakeLists.txt:

            The package libpng is compatible with built-in CMake targets:
            
                find_package(PNG REQUIRED)
                target_link_libraries(main PRIVATE PNG::PNG)

            Здесь вместо main лучше подставить ${PROJECT_NAME}

            Но под Windows надо ещё догадаться, что нужны ещё вот такие строки:

            if(WIN32)
            find_path(PNG_INCLUDE_DIR PNG)
            target_include_directories(${PROJECT_NAME} PRIVATE ${PNG_INCLUDE_DIR})
            endif(WIN32)

            И с каждой библиотекой приходится танцы с бубном устраивать, чтобы понять, что именно надо включить в CMakeLists.txt, чтобы библиотека подшилась. К сожалению, файл вроде usage есть далеко не для всех библиотек.

            А еще, если библиотека не header only, она под Windows статически обычно не подшивается. Конкретно для PNG после сборки рядом с EXE-файлом ещё оказываются libpng16.dll и zlib1.dll - не подскажете, как добиться того, чтобы они статически прилинковались в исполняемый файл?


            1. Playa
              15.01.2025 09:51

              Но под Windows надо ещё догадаться, что нужны ещё вот такие строки

              Не нужны. Если без этого не работает - что-то криво настроили или не почистили CMakeCache.txt.
              90% ошибок из-за двух вещей:

              • Вместо использования предоставленного vcpkg тулчейна, как указано в документации, люди натравливают CMake прямо на папку vcpkg_installed.

              • При использовании тулчейна указывают его после вызова project(), то есть слишком поздно.

              не подскажете, как добиться того, чтобы они статически прилинковались в исполняемый файл?

              Какие библиотеки будут собираться решается триплетом.
              https://learn.microsoft.com/en-us/vcpkg/concepts/triplets
              В вашем случае это будет x64-windows-static или x64-windows-static-md, если ваш проект линкуется с CRT динамически.

              PS: PNG с недавних пор сам экспортирует свои таргеты и это учтено в usage, обновитесь :)


  1. klirichek
    15.01.2025 09:51

    Как вы верно заметили, внешние библиотеки, как правило, сводятся к двум сущностям: самой библиотеке (.dll, .so, .lib, .a, whatever) и хедерам.

    Поэтому обычно требуется задать link-library и include-directories.

    Такой подход чреват ошибками, поэтому примерно 10 лет назад в cmake вместо непосредственных зависимостей настоятельно рекомендуют использовать таргеты.

    Суть в том, что вы описываете внешнюю либу в терминах отдельного целостного таргета. В его свойствах указываете путь к самой либе, путь к инклюдам и т.д. (вплоть до специфических ключей компиляции, которые должны быть у проекта, если он использует эту либу). Т.е. вместо ваших упражнений:

    target_link_libraries(engine PRIVATE
      ${CMAKE_CURRENT_SOURCE_DIR}/lib/lib-mingw-w64/libglfw3.a)
    # Or
    target_link_libraries(engine PRIVATE
      ${CMAKE_CURRENT_SOURCE_DIR}/lib/lib-vc2022/glfw3.lib)
    
    target_include_directories(engine PRIVATE 
      ${CMAKE_CURRENT_SOURCE_DIR}/include)

    нынче (уж 10 лет, как) советуют делать

    add_library(glfw3 STATIC IMPORTED)
    set_target_properties(glfw3 PROPERTIES
      IMPORTED_LOCATION "glfw3/lib/lib-vc2022/glfw3.lib"
      INTERFACE_COMPILE_DEFINITIONS "можно ещё и опций компилятора напихать..."
      INTERFACE_INCLUDE_DIRECTORIES "glfw3/include"
    )
    
    target_link_libraries(engine PRIVATE glfw3)

    Первая часть (с определением внешней либы) может быть в проекте, а может импортироваться снаружи, для этого примерно такой по сути код кладётся в файл `glfw3-config.cmake` в одно из известных мест, и затем cmake его подтягивает, когда вы говорите find_package (glfw3). По итогу в проекте у вас остаётся find_package + последующее target_link_libraries. Никаких жёстко привязанных деталей самой библиотеки нет, они живут снаружи.

    Суть в том, что вы подключаете либу единственным target_link_libraries, и получаете сразу всё - и линки, и инклюды, и прочие особенности. Шансы что-то упустить исчезают.

    (то же работает и для headers-only, и для shared. Просто везде нынче таргеты, а разные ручные линки/инклюды уже не комильфо).

    Ну и про glob вам всё расскажет гугл/яндекс по запросу `Why cmake file GLOB evil`


    1. buldo
      15.01.2025 09:51

      Где-нибудь бы ещё писали заранее как делать и как не делать. А то приходится такие знания по крупицам собирать


      1. klirichek
        15.01.2025 09:51

        Оно "где-то" периодически всплывает; больше на разных конференциях и в виде слайдов. Например, https://indico.gsi.de/event/13328/contributions/56722/attachments/37710/50573/modern_cmake_cpp_user_group_nov21_dennis_dklein.pdf
        Можно ещё подобного найти по фразам "modern cmake" и подобным. Главное, на даты выхода смотреть, потому что некоторые из тех 'modern' уже давно устарели.
        По версиям - это примерно закрутилось с cmake 3.14 и далее.


        1. buldo
          15.01.2025 09:51

          Моё недоумение скорее связано с непониманием, почему в документации самого CMake нет best practices и примеров использования.


          1. Playa
            15.01.2025 09:51

            Я называю этот феномен "покупайте мою книжку", потому что в той самой книге это всё есть :)
            Professional CMake by Craig Scott.


    1. AnPosy Автор
      15.01.2025 09:51

      Спасибо за предложение. Я постарался дополнить статью в соответствии с ним.
      С выделением отдельной библиотеки я согласен, однако считаю, что GLOB не так плох как привыкли думать, особенно для маленьких проектов, поэтому я только добавил абзац о возможных минусах.


      1. voldemar_d
        15.01.2025 09:51

        glob вполне хорош, но надо хорошо понимать, какие последствия могут вылезти в определенных случаях. И, что плохо, вылезти они могут далеко не сразу.


    1. Jijiki
      15.01.2025 09:51

      а если таргеты - проект - и каждый подпроект прописан в проекте глоб вроде упрощает сборку, просто эти подпроекты надо включить в перенос на гит например, но на винде я не тестил

      тоесть соберутся сначала таргеты, потом начнется сборка подпроектов которые входят в проект

      тогда куча файлов проекта будет кор-основой для каждого нового подпроекта

      при том такая структура позволит делать бинарники на каждую стадию состояния кор основы проекта, они будут как проектики текущего состояния со своим бинарников в папке bin

      тоесть велосипед состоит в том что если нужнен новый снимок состояния возможностей в новый бинарник с кодом мы пишем новый такой проектик где показываем то что хотели показать новыми написаными возможностями состояния кодовой базы. звучит тофтологично и вроде нет магии, но имеет место быть


    1. sabudilovskiy
      15.01.2025 09:51

      GLOB уже давно починили, никакой проблемы его использовать нет.


  1. Jijiki
    15.01.2025 09:51

    у меня есть тоже на вулкане сценка - на С и на С++. я так и не понял как рендерить сцены с многими обьектами, я в этой архитектуре вулкана тупо запутался пока следовал туториалам и пытался потом вникнуть, допустим у меня есть сцена с 1 текстурой и 3 модельками, но как из этого делать рендер аля опенЖЛ пока не допираю, не удобно очень, но круто сделано

    (тоесть тупо запутался в этих дескриптор пулах буферах, что куда подключать чтоб придти к функционалу загрузил модельку текстуру кинул в шейдер и управляю моделькой допустим на террене с кубмапой и прочее, а по отдельности вроде понял)


  1. Tujh
    15.01.2025 09:51

    Так как Vulkan - это полноценный SDK (набор инструментов разработки), то он содержит в себе библиотеки, заголовки, документацию, примеры и инструменты. Поэтому его приходится устанавливать отдельно как программу.

    Совсем не обязательно. Можно использовать volk и Vulkan-Headers.

    Тогда можно будет фетчить их из Cmake, а запускаться Vulkan будет динамически, без необходимости линковки со статическими библиотеками. Работает для Linux и Windows, должно работать для Android. Как-то можно заставить работать для MacOSX но там в любом случае нужно будет возиться с MoltenVK.