LearnOpenGL 及 ShaderToy 的 CMake 构建框架


为了复习 OpenGL(主要是看到 shadertoy 上有好玩的着色器),所以打算重新写一下构建框架来适配 shadertoy 到本地 GLSL 代码的转换,这样 shadertoy 上有什么好玩的着色器都可以直接复制下来在本地运行(当然片段着色器里面有一些坐标还是需要转换一下的)。

构建目标

  • 构建源码目录前,扫描导入库目录下的所有动态库和静态库并引入为当前 CMake 的 target,这样其他二进制可执行文件目标在构建的时候可以随时链接全局 target 名称
  • 适配 Linux 和 Windows 系统平台的动态库和静态库链接编译,Windows 平台基于 MSVC 编译工具链的情况比较复杂一些,有一些坑
  • 允许灵活的添加测试目录或者指定要构建的单元测试源代码
  • 混合编译 C/C++
  • 指定每一个 shader 主程序编译所需要链接的动态库或者静态库

具体框架

主目录结构如下:

F:\FREDOM\WORKSPACE\PLAY-CC\SHADERTOY
├─.vscode
├─app
│  ├─learnopengl
│  └─shadertoy
├─assets
│  ├─audio
│  ├─model
│  ├─shader
│  │  ├─learnopengl
│  │  │  ├─frag
│  │  │  ├─geom
│  │  │  └─vert
│  │  └─shadertoy
│  └─texture
├─build
├─cmake
├─include
│  ├─assimp
│  ├─dummy
│  ├─glad
│  ├─GLFW
│  ├─glm
│  ├─KHR
│  ├─learnopengl
│  ├─stb_image
│  └─utils
├─lib
│  ├─assimp
│  ├─glfw3
│  └─opengl32
├─src
│  ├─dummy
│  ├─glad
│  ├─stb_image
│  └─utils
└─test
    ├─log
    └─misc
  • app:存放每个 main 函数入口源文件,一个文件就是一个应用程序的入口点,可以编写多个入口 main ,这当然是为了方便测试不同的 shadertoy 代码了,如果只能编译一个 main 的话太不方便了(所以用 VScode 比 VisualStudio 灵活)
  • assets:资源文件目录,比如纹理贴图、模型材质、着色器、音频
  • build:构建目录,存放构建的动态库静态库以及构建完之后安装(install 的概念大概就是复制构建产物到一个单独的目录下方便取用)
  • cmake:存放一些 cmake 脚本,比如自动搜索头文件目录或者扫描库目录下的库文件
  • include:存放所有头文件
  • lib:外部引入的第三方库(静态或动态),比如 glfw3、assimp 等等
  • src:头文件对应的实现文件,这一般是项目内部的构件库的实现文件目录,因为外部导入的头文件一般使用的当然是对应的构建好的库文件,实现都已经在库文件中了而不是实际的源代码,不然每次编译几百个源代码那真是有的好受的
  • test:存放各种单元测试文件

根目录

首先是项目根目录的 CMakeLists.txt ,既然是根目录的配置文件,当然是负责一些全局的设置和引入其他子目录了,不可能根目录下的一个配置文件完成所有构建目标的配置,每个目录下的源文件构建由子目录对应的构建配置 CMakeLists.txt 决定如何完成,根目录应该设置好当前编译系统名称以及全局的宏开关。

具体内容如下:

CMAKE_MINIMUM_REQUIRED(VERSION 3.21)
PROJECT(temp)

MESSAGE("build system: ${CMAKE_SYSTEM_NAME}")
MESSAGE("${CMAKE_CURRENT_BINARY_DIR}")
SET(CMAKE_CXX_STANDARD 17)
SET(CAMKE_C_STANDARD 17)

SET(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}/install)
# set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) # binary executable
# set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) # shared library
# set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) # static library

# help vscode find compiler_command.json
ADD_DEFINITIONS(-DCMAKE_EXPORT_COMPILE_COMMANDS=ON)

# other define in source code
ADD_COMPILE_DEFINITIONS(CXX_COMPILER_ID_${CMAKE_CXX_COMPILER_ID})
ADD_COMPILE_DEFINITIONS(C_COMPILER_ID_${CMAKE_C_COMPILER_ID})
STRING(TOUPPER ${CMAKE_BUILD_TYPE} build_type_str)
ADD_COMPILE_DEFINITIONS(${build_type_str})

IF(CMAKE_SYSTEM_NAME STREQUAL "Windows" AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    MESSAGE("compiler tool chain MSVC, all symbols will export by default")
    SET(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
ENDIF()



INCLUDE(cmake/macro.cmake)

INCLUDE(import.cmake)

ADD_SUBDIRECTORY(src)
ADD_SUBDIRECTORY(app)
ADD_SUBDIRECTORY(test)

其中这一段代码:

IF(CMAKE_SYSTEM_NAME STREQUAL "Windows" AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
    MESSAGE("compiler tool chain MSVC, all symbols will export by default")
    SET(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE)
ENDIF()

是为了在 Windows 下使用 MSVC 编译时,默认导出所有函数符号,C++ 背景下接口的声明和实现就是函数的头文件和动态库或静态库,Windows 下默认函数符号是不导出的,必须手动使用 __declspec(dllexport) 来标注某个函数的导出,这是对于库的构建者来说的,同时对于使用该库的用户,如果使用了分发的接口头文件中的某个接口(函数),也即头文件中给出的接口函数都是开发者编译的动态库中允许导出的符号,则使用者使用函数之前也需要添加属性 __declspec(dllimport) (不知道这么理解对不对);如果 MSVC 编译静态库的话就没有这么麻烦,编译完输出文件之后在链接阶段直接把静态库链接到二进制可执行文件,没有什么导出不导出的问题了。

但是不幸的是因为我自己还有一个子目录是写了一个 C/C++ 的打印日志功能的,其中 C 的打印日志使用了全局的缓冲数组,因此在头文件中进行了 extern 的全局变量声明;注意这是一个常见的错误:不要在头文件中直接声明全局变量,否则容易发生重定义错误,这不是 #ifndef#define 能解决的重复包含问题(防止一个编译单元内多次包含,但是现在的情况是多个编译单元),而是在链接阶段如果存在多个使用了该头文件对应的链接库的链接输出文件,后续再次参与链接,那么多个这样的文件中的导出符号表将使得链接器无法选择重名的符号。

src 目录

注意,先包含 src 目录再包含 app 目录,因为 src 下可能有构建的目标被 app 中的主应用程序依赖,比如我这里的 log 的一些功能就是 test 中所依赖的实现,那么必须先编译 src 下面的目标,这样库目标存在之后才能被其他子目录的编译构建链接使用。

SET(lib_type SHARED)
# SET(lib_type STATIC)

# build dummy
FILE(
    GLOB module_dummy_src_list
    dummy/*.cpp
    dummy/*.cc
    dummy/*.c
)
PRINT_LIST("${module_dummy_src_list}" "MODULE [dummy] SRC" "")
ADD_LIBRARY(dummy ${lib_type} ${module_dummy_src_list})
INSTALL(
    TARGETS dummy 
    RUNTIME DESTINATION bin
    ARCHIVE DESTINATION lib
)

# build utils
FILE(
    GLOB module_utils_src_list
    utils/*.cpp
    utils/*.cc
    utils/*.c
)
PRINT_LIST("${module_utils_src_list}" "MODULE [utils] SRC" "")
ADD_LIBRARY(utils ${lib_type} ${module_utils_src_list})
IF(lib_type STREQUAL "SHARED")
    # this is for MSVC usage to export global data symbols
    # CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS only exports method
    IF(CMAKE_SYSTEM_NAME STREQUAL "Windows" AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
        TARGET_COMPILE_DEFINITIONS(
            utils
            PRIVATE DLLCOMPILE # can not use PUBLIC attr? That's weird...
        )
    ENDIF()
ENDIF()
INSTALL(
    TARGETS utils 
    RUNTIME DESTINATION bin
    ARCHIVE DESTINATION lib
)

# build other
FILE(
    GLOB module_other_src_list
    ./*.cpp
    ./*.cc
    ./*.c
)
PRINT_LIST("${module_other_src_list}" "MODULE [other] SRC" "")
ADD_LIBRARY(other ${lib_type} ${module_other_src_list})
INSTALL(
    TARGETS other 
    RUNTIME DESTINATION bin
    ARCHIVE DESTINATION lib
)


# build glad
FILE(
    GLOB module_glad_src_list
    glad/*.cpp
    glad/*.cc
    glad/*.c
)
PRINT_LIST("${module_glad_src_list}" "MODULE [glad] SRC" "")
ADD_LIBRARY(glad ${lib_type} ${module_glad_src_list})
IF(lib_type STREQUAL "SHARED")
    TARGET_COMPILE_DEFINITIONS(
        glad
        PUBLIC GLAD_GLAPI_EXPORT
        PRIVATE GLAD_GLAPI_EXPORT_BUILD
    )
ENDIF()
INSTALL(
    TARGETS glad
    RUNTIME DESTINATION bin
    ARCHIVE DESTINATION lib
)

# build stb_image
FILE(
    GLOB module_stb_image_src_list
    stb_image/*.cpp
    stb_image/*.cc
    stb_image/*.c
)
PRINT_LIST("${module_stb_image_src_list}" "MODULE [stb_image] SRC" "")
ADD_LIBRARY(stb_image ${lib_type} ${module_stb_image_src_list})
INSTALL(
    TARGETS stb_image
    RUNTIME DESTINATION bin
    ARCHIVE DESTINATION lib
)

这里选择的是构建动态库,其实 CMake 自己默认有一个这个宏的 BUILD_SHARED_LIBS 其实用哪个都行,这里我是自定义了 lib_type 变量,方便构建的时候直接传入这个变量代表构建库类型。

INSTALL 这些构建好的目标是必须的,二进制可执行文件如果使用的是动态库链接,那么运行时会在当前的目录下寻找链接库或者去系统环境变量指定的目录下寻找,为了在 VScode 中能够 F5 一键 Debug ,构建的动态库要放到编译产物的输出安装目录了,静态库的话安不安装倒无所谓……

这里当然可以写一个 FOREACH 循环扫描当前目录的每个子目录然后执行构建,但是这样有时候想单独指定某个库是动态的还是静态的就不方便了,这里我还是拆开每个子目录单独指定构建流程。

后续如果有其他的自定义实现函数模块,可以在 src 目录下新建一个子目录存放模块相关的实现文件,然后在 src 目录下的 CMakeLists.txt 文件里面添加对应的模块构建流程,就是把其他模块构建流程复制一遍改一下路径。

app 目录

app 目录就是指定要构建哪些主程序的入口文件,然后链接所需的库,再把链接过的动态库放到和输出二进制文件同一个目录方便调试。

# user should be responsible for installing the dynamic
# library that they used to link with any exe bin

# ============================= main =============================
SET(lib_inn_static)
SET(lib_inn_shared)
SET(lib_ext_static)
SET(lib_ext_shared)
SET(
    lib_all
    ${lib_inn_static}
    ${lib_inn_shared}
    ${lib_ext_static}
    ${lib_ext_shared}
)
ADD_EXECUTABLE(main main.cpp)
IF(lib_all)
    TARGET_LINK_LIBRARIES(main ${lib_all})
    # move dll/so to bin dir
    # for inner lib and external static
    INSTALL(
        TARGETS ${lib_inn}
        RUNTIME DESTINATION bin
        ARCHIVE DESTINATION lib
    )
ENDIF()
INSTALL(TARGETS main RUNTIME DESTINATION bin)


# ============================= learnopengl =============================
SET(
    learnopengl_src_list
    learnopengl/triangle.cpp
    learnopengl/texture.cpp
    learnopengl/transform.cpp
    learnopengl/coordinate.cpp
    learnopengl/camera.cpp
    learnopengl/framebuffer.cpp
    learnopengl/model_load.cpp
)

SET(lib_inn_static)
SET(lib_inn_shared glad stb_image)
SET(lib_ext_static opengl32)
SET(lib_ext_shared glfw3 assimp-vc143-mtd)
SET(
    lib_all
    ${lib_inn_static}
    ${lib_inn_shared}
    ${lib_ext_static}
    ${lib_ext_shared}
)

FOREACH(_src_file ${learnopengl_src_list})
    GET_FILENAME_COMPONENT(_src_name ${_src_file} NAME_WE)
    ADD_EXECUTABLE(${_src_name} ${_src_file})
    IF(lib_all)
        TARGET_LINK_LIBRARIES(${_src_name} ${lib_all})
    ENDIF()
    INSTALL(TARGETS ${_src_name} RUNTIME DESTINATION bin)
ENDFOREACH()

IF(lib_all)
    # move dll/so to bin dir
    # for internal targets
    INSTALL(
        TARGETS ${lib_inn_shared} ${lib_inn_static}
        RUNTIME DESTINATION bin
        ARCHIVE DESTINATION lib
    )
    # for external targets
    INSTALL(
        IMPORTED_RUNTIME_ARTIFACTS ${lib_ext_shared}
    )
ENDIF()


# ============================= shadertoy =============================
SET(
    shadertoy_src_list
    shadertoy/isovalues.cpp
    shadertoy/simple.cpp
)

SET(lib_inn_static)
SET(lib_inn_shared glad stb_image)
SET(lib_ext_static opengl32)
SET(lib_ext_shared glfw3 assimp-vc143-mtd)
SET(
    lib_all
    ${lib_inn_static}
    ${lib_inn_shared}
    ${lib_ext_static}
    ${lib_ext_shared}
)

FOREACH(_src_file ${shadertoy_src_list})
    GET_FILENAME_COMPONENT(_src_name ${_src_file} NAME_WE)
    ADD_EXECUTABLE(${_src_name} ${_src_file})
    IF(lib_all)
        TARGET_LINK_LIBRARIES(${_src_name} ${lib_all})
    ENDIF()
    INSTALL(TARGETS ${_src_name} RUNTIME DESTINATION bin)
ENDFOREACH()

IF(lib_all)
    # move dll/so to bin dir
    # for internal targets
    INSTALL(
        TARGETS ${lib_inn_shared} ${lib_inn_static}
        RUNTIME DESTINATION bin
        ARCHIVE DESTINATION lib
    )
    # for external targets
    INSTALL(
        IMPORTED_RUNTIME_ARTIFACTS ${lib_ext_shared}
    )
ENDIF()

同样也是可以收集所有子目录然后统一执行构建流程的,但问题是不是每个源文件都依赖于相同的库,虽然说直接把所有库放到一个列表里面让链接器自己去按需取用也没问题,但是感觉不够优雅而且不够精准,我就是想指定每个每个主程序所依赖的库……

比如这里分了 内部静态、内部动态、外部静态、外部动态 四个类型的库列表,按照源文件所依赖的函数填写对应的库名称即可。

SET(lib_inn_static)
SET(lib_inn_shared glad stb_image)
SET(lib_ext_static opengl32)
SET(lib_ext_shared glfw3 assimp-vc143-mtd)
SET(
    lib_all
    ${lib_inn_static}
    ${lib_inn_shared}
    ${lib_ext_static}
    ${lib_ext_shared}
)

import.cmake

这个文件主要是为了扫描所有的头文件目录和外部库,然后告诉 CMake 。如果不告诉 CMake 头文件的目录路径而是使用默认的 VScode C++ Intellisense ,那么就只能使用双引号+头文件的文件名来引入头文件,可是很多时候头文件都是带相对路径的,所以必须告诉 VScode 的 C++ Intellisense 插件这些信息才不会弹出头文件包含错误提示(虽然编译的时候没事,但是看着总是不顺眼的嘛)。

# 包含头文件目录
FIND_HDRS(include return_hdr_dir_list)
PRINT_LIST("${return_hdr_dir_list}" "HEADER" "")
INCLUDE_DIRECTORIES(${return_hdr_dir_list})

SET(lib_import_shared_list)
SET(lib_import_static_list)

SET(lib_shared_suffix "dll") # linux: so
SET(lib_static_suffix "lib") # linux: a

LINK_DIRECTORIES(lib/glfw3)
FIND_LIBS(lib/glfw3 ${lib_shared_suffix} return_lib_name_list return_lib_path_list)
MAKE_LIBS_TARGET("${return_lib_name_list}" "${return_lib_path_list}" SHARED)
LIST(APPEND lib_import_shared_list ${return_lib_name_list})

LINK_DIRECTORIES(lib/assimp)
FIND_LIBS(lib/assimp ${lib_shared_suffix} return_lib_name_list return_lib_path_list)
MAKE_LIBS_TARGET("${return_lib_name_list}" "${return_lib_path_list}" SHARED)
LIST(APPEND lib_import_shared_list ${return_lib_name_list})


LINK_DIRECTORIES(lib/opengl32)
FIND_LIBS(lib/opengl32 ${lib_static_suffix} return_lib_name_list return_lib_path_list)
MAKE_LIBS_TARGET("${return_lib_name_list}" "${return_lib_path_list}" STATIC)
LIST(APPEND lib_import_static_list ${return_lib_name_list})

PRINT_LIST("${lib_import_shared_list}" "IMPORT LIB SHARED" "")
PRINT_LIST("${lib_import_static_list}" "IMPORT LIB STATIC" "")

同理每次导入了新的库和头文件,应该在这里添加新的扫描(头文件目录是递归扫描的,但是库目录我没有这样做)。

其他 CMake 函数

还有一些上面用到的 CMake 函数宏在 cmake 目录下,主要是搜索头文件,库文件以及打印列表的函数。

FUNCTION(FIND_LIBS lib_dir suffix return_lib_name_list return_lib_path_list)
    UNSET(return_lib_name_list CACHE)
    UNSET(return_lib_path_list CACHE)

    FILE(
        GLOB lib_path_list
        ${lib_dir}/*.${suffix}
    )

    FOREACH(_lib_path ${lib_path_list})
        GET_FILENAME_COMPONENT(_lib_name ${_lib_path} NAME_WE)
        # MESSAGE("_lib_name ${_lib_name}")
        LIST(APPEND lib_name_list ${_lib_name})
    ENDFOREACH(_lib_path ${lib_path_list})

    # MESSAGE("lib_name_list ${lib_name_list}")
    # MESSAGE("lib_path_list ${lib_path_list}")
    SET(return_lib_name_list ${lib_name_list} PARENT_SCOPE)
    SET(return_lib_path_list ${lib_path_list} PARENT_SCOPE)
ENDFUNCTION()

FUNCTION(MAKE_LIBS_TARGET lib_name_list lib_path_list lib_type)
    IF(NOT lib_type STREQUAL "SHARED" AND NOT lib_type STREQUAL "STATIC")
        MESSAGE(FATAL_ERROR "unknown lib type ${lib_type}")
    ENDIF()

    LIST(LENGTH lib_name_list num_lib_name)
    LIST(LENGTH lib_path_list num_lib_path)
    IF(NOT num_lib_name EQUAL num_lib_path)
        MESSAGE(FATAL_ERROR "number of name and path of libs not equal")
    ENDIF()

    FOREACH(i RANGE 0 ${num_lib_name})
        IF(${i} EQUAL ${num_lib_name})
            BREAK()
        ENDIF()

        LIST(GET lib_name_list ${i} _lib_name)
        LIST(GET lib_path_list ${i} _lib_path)
        ADD_LIBRARY(${_lib_name} ${lib_type} IMPORTED GLOBAL)
        SET_PROPERTY(
            TARGET ${_lib_name}
            PROPERTY IMPORTED_LOCATION ${_lib_path}
        )
        # only for windows dll, that's suck
        IF(CMAKE_SYSTEM_NAME STREQUAL "Windows" AND lib_type STREQUAL "SHARED")
            GET_FILENAME_COMPONENT(_lib_dir ${_lib_path} DIRECTORY)
            SET_PROPERTY(
                TARGET ${_lib_name}
                PROPERTY IMPORTED_IMPLIB ${_lib_dir}/${_lib_name}.lib # may be this name rule?
            )
        ENDIF()
    ENDFOREACH()

ENDFUNCTION()

FUNCTION(FIND_HDRS hdr_dir return_hdr_dir_list)
    UNSET(hdr_dir_list CACHE)
    SET(hdr_dir_list)
    FILE(
        GLOB_RECURSE hdr_path_list
        ${hdr_dir}/*.h
        ${hdr_dir}/*.hpp
    )
    SET(hdr_dir_list ${hdr_dir})
    FOREACH(_hdr_path ${hdr_path_list})
        GET_FILENAME_COMPONENT(_hdr_dir ${_hdr_path} PATH)
        LIST(APPEND hdr_dir_list ${_hdr_dir})
    ENDFOREACH(_hdr_path ${hdr_path_list})
    LIST(REMOVE_DUPLICATES hdr_dir_list)
    SET(return_hdr_dir_list ${hdr_dir_list} PARENT_SCOPE)
ENDFUNCTION()


# print list item
FUNCTION(PRINT_LIST list_item title prefix)
    IF(NOT list_item OR (list_item STREQUAL ""))
        RETURN()
    ENDIF()
    MESSAGE("┌────────────────── ${title}")
    FOREACH(item ${list_item})
        MESSAGE("│ ${prefix} ${item}")
    ENDFOREACH()
    MESSAGE("└──────────────────]\n")
ENDFUNCTION()

使用框架

因为是在 Windows 平台下编译的,所以能用 MSVC 当然还是用 MSVC,虽然感觉有些难用就是了。VScode 的 CMake 插件可以选择构建的工具链,比如 MinGW 或者 MSVC ,MinGW 的 g++/gcc 构建比较简单,这里我主要是测试 MSVC 构建流程。所以如果使用 MSVC 的话,那么是无法直接通过 VScode 打开项目目录构建的:

  • 此时 C++ Intellisense 没有 MSVC 的扫描权限,无法找到标准库头文件诸如 iostream 等
  • 此时编译可以完成,但是编译出来的动态库不具有链接性(可能是我个人的原因?)

正确的操作方式应该是打开 VisualStudio 开发者命令行工具,然后 cd 到项目的目录下,再使用 code . 命令在此目录下打开 VScode ,这样才能检索到 MSVC 和 WindowsSDK 的头文件目录以及标准库。

CMake 配置时的输出如下:

[main] 正在配置项目: shadertoy 
[proc] 执行命令: E:\CMake\bin\cmake.EXE -DCMAKE_EXPORT_COMPILE_COMMANDS=ON --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -SF:/fredom/workspace/play-cc/shadertoy -Bf:/fredom/workspace/play-cc/shadertoy/build -G "Visual Studio 17 2022" -T host=x64 -A x64
[cmake] Not searching for unused variables given on the command line.
[cmake] -- Selecting Windows SDK version 10.0.22621.0 to target Windows 10.0.19045.
[cmake] -- The C compiler identification is MSVC 19.38.33134.0
[cmake] -- The CXX compiler identification is MSVC 19.38.33134.0
[cmake] -- Detecting C compiler ABI info
[cmake] -- Detecting C compiler ABI info - done
[cmake] -- Check for working C compiler: E:/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe - skipped
[cmake] -- Detecting C compile features
[cmake] -- Detecting C compile features - done
[cmake] -- Detecting CXX compiler ABI info
[cmake] -- Detecting CXX compiler ABI info - done
[cmake] -- Check for working CXX compiler: E:/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.38.33130/bin/Hostx64/x64/cl.exe - skipped
[cmake] -- Detecting CXX compile features
[cmake] -- Detecting CXX compile features - done
[cmake] build system: Windows
[cmake] F:/fredom/workspace/play-cc/shadertoy/build
[cmake] compiler tool chain MSVC, all symbols will export by default
[cmake] ┌────────────────── HEADER
[cmake] │  include
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/GLFW
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/KHR
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/assimp
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/assimp/Compiler
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/dummy
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/dummy/dummy2
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glad
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glm
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glm/detail
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glm/ext
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glm/gtc
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glm/gtx
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/glm/simd
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/learnopengl
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/stb_image
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/include/utils
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── IMPORT LIB SHARED
[cmake] │  glfw3
[cmake] │  assimp-vc143-mtd
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── IMPORT LIB STATIC
[cmake] │  opengl32
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── MODULE [dummy] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/dummy/dummy.cpp
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/dummy/dummy2.cpp
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── MODULE [utils] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/utils/clog.c
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/utils/cpplog.cpp
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── MODULE [other] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/./add.cpp
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/./mathnipet.cpp
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── MODULE [glad] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/glad/glad.c
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── MODULE [stb_image] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/src/stb_image/stb_image.cpp
[cmake] └──────────────────]
[cmake] 
[cmake] ┌────────────────── TEST [LOG] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/test/log/clog_test.c
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/test/log/cpplog_test.cpp
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/test/log/mixlog_test.cpp
[cmake] └──────────────────]
[cmake] 
[cmake] -- Configuring done (4.9s)
[cmake] ┌────────────────── TEST [misc] SRC
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/test/misc/enummap.cpp
[cmake] │  F:/fredom/workspace/play-cc/shadertoy/test/misc/include.cpp
[cmake] └──────────────────]
[cmake] 
[cmake] -- Generating done (0.1s)
[cmake] -- Build files have been written to: F:/fredom/workspace/play-cc/shadertoy/build
[visual-studio] 为 E:\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat 修补从 C:\Program Files (x86)\Windows Kits\10\bin\x64 到 C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64 的 Windows SDK 路径

编译完所有项目之后,可以直接在 install 目录里面的 bin 目录双击执行可调式文件,或者直接在 VScode 里面 F5 一键调试。目前验证的 learnopengl 教程只有几个,没有全部验证,不过也就是简单的把 LearnOpenGL 的代码拷贝到本地做一下资源路径的修改罢了,因为本意还是给 ShaderToy 准备的,LearnOpenGL 是用来验证 MSVC 生成的其他动态库可以链接使用,功能正常。

实际效果

摄像机坐标变换

典型的 FPS 移动摄像机

使用 assimp 库加载模型

背包模型导入 obj 以及材质文件和纹理贴图之后的效果

shadertoy 测试 framebuffer 离屏渲染

帧缓冲重新作为片段着色器输出的测试

其他

相关的代码和 MSVC 2022 编译的 GLAD、assimp 库之类的放在代码仓库了,有点大,200 多 MB,好像 GitHub 最多支持 5 GB 的仓库大小,其实主要是 assets 资源文件里面纹理贴图有点多,构建出来的动态库本身倒不是很大。下载代码下来之后记得把 build 目录删除清空,重新执行 CMake 配置缓存。注意代码仓库里面包含的 lib 下的动态库都是 MSVC 2022 编译构建的,如果使用 MinGW 的话,需要自己把对应的 GLAD 还有 assimp 库用 MinGW 生成动态库,复制到项目的 lib 目录下对应的子目录。Windows 下编译动态库虽然也会输出 lib 文件,但是这个文件是用于动态库链接的时候找导出符号的,和平常的 lib 静态库文件不是一个东西……所以如果在 Windows 下使用 MSVC 构建程序动态链接,记得把和动态库同名的 .lib 后缀的符号导出文件一起拷贝过去,同时在 CMake 里面指定该动态库的导入符号属性文件路径。

CMake 现在已经为 Windows 平台下的函数动态库自动导出所有函数符号实现了预定义宏开关 CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE ,只要该宏被设置了,那么所有函数都可以像 GNU g++/gcc 那样生成的动态库一样自动导出,但是对于这些函数所依赖的全局数据变量,仍然需要使用 __declspec(dllexport/dllimport) 来完成符号的导出和引入,具体可以参考 这篇文章

For global data symbols, __declspec(dllimport) must still be used when compiling against the code in the DLL

另外在编译 OpenGL 项目的时候,因为需要用到 glad 来加载 OpenGL 的函数指针,所以我想把 glad.c 以及 glad.h 还有相关的头文件编译为动态库,但是尽管我使用了上述的 CMake 预定义宏,编译能完成但是链接二进制文件的时候,还是报错无法链接到动态库的函数,这困扰了我几个小时,后来在 glad 官方仓库的 issues 里面看到有人回答了这个问题,原来 glad.h 里面对所有函数指针的导出都是以全局公共数据变量而不是函数变量定义的,上文也说到,CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=TRUE 预定义宏只能导出函数符号,全局变量符号还是需要用到 __declspec ,所以对于该动态库的编译,必须在编译的时候添加额外的宏定义参数:

target_compile_definitions(glad PUBLIC GLAD_GLAPI_EXPORT PRIVATE GLAD_GLAPI_EXPORT_BUILD)

因为 glad.h 这个头文件里面定义了这么一段:

#ifndef GLAPI
# if defined(GLAD_GLAPI_EXPORT)
#  if defined(_WIN32) || defined(__CYGWIN__)
#   if defined(GLAD_GLAPI_EXPORT_BUILD)
#    if defined(__GNUC__)
#     define GLAPI __attribute__ ((dllexport)) extern
#    else
#     define GLAPI __declspec(dllexport) extern
#    endif
#   else
#    if defined(__GNUC__)
#     define GLAPI __attribute__ ((dllimport)) extern
#    else
#     define GLAPI __declspec(dllimport) extern
#    endif
#   endif
#  elif defined(__GNUC__) && defined(GLAD_GLAPI_EXPORT_BUILD)
#   define GLAPI __attribute__ ((visibility ("default"))) extern
#  else
#   define GLAPI extern
#  endif
# else
#  define GLAPI extern
# endif
#endif

真的有点坑人……

相关推荐

  1. 关于CMAKE构建C/C++遇到问题汇总

    2024-06-18 14:18:05       35 阅读
  2. 如何使用 CMake构建一个包含子目录 C++ 项目

    2024-06-18 14:18:05       34 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-18 14:18:05       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-18 14:18:05       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-18 14:18:05       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-18 14:18:05       18 阅读

热门阅读

  1. 编程连接主板:深入探索与实践的技术之旅

    2024-06-18 14:18:05       8 阅读
  2. 程序员做电子书产品变现的复盘(5)

    2024-06-18 14:18:05       7 阅读
  3. Halcon C++ XLD 数据写入图片

    2024-06-18 14:18:05       8 阅读
  4. webpack 自动清理 dist 文件夹的两种实现方法

    2024-06-18 14:18:05       6 阅读
  5. 生产环境下部署微调的10条戒律

    2024-06-18 14:18:05       6 阅读
  6. 常用原语介绍

    2024-06-18 14:18:05       8 阅读
  7. Redis内存数据库

    2024-06-18 14:18:05       6 阅读
  8. 【React】useState 的原理

    2024-06-18 14:18:05       7 阅读