前言
上一回合讲到 C/C++ 开发环境的搭建,基础搞好了,这里终于可以进入正题了,废话不多话,先找 gpt 帮忙开个头,it goes like this:)
PROMPT:介绍一下GLFW和GLAD
ANSWER:GLFW和GLAD是两个常用的库,用于在现代OpenGL应用程序中进行窗口管理和加载OpenGL函数。
1、 GLFW(OpenGL Framework)是一个轻量级的开源库,用于创建和管理窗口、处理用户输入(如键盘、鼠标、游戏手柄等),以及获取时间间隔。它提供了一个简单的接口,方便开发者快速搭建OpenGL应用程序的窗口环境。GLFW跨平台,可以在Windows、MacOS和Linux等操作系统上使用。
2、 GLAD(OpenGL Loading Library)是一个开源的库,用于加载OpenGL函数。在早期版本的OpenGL中,需要手动查找和加载函数指针才能使用OpenGL的功能。而GLAD简化了这个过程,它自动生成了跨平台的OpenGL加载器代码,可以自动获取系统支持的OpenGL版本和扩展,并将相关函数指针加载到程序中,方便开发者使用OpenGL的各种功能。
glfw和glad常常被一起使用。开发者可以使用GLFW创建窗口、处理用户输入,然后使用GLAD加载特定OpenGL版本的函数,并在GLFW创建的窗口环境中使用这些函数实现图形渲染以及其他OpenGL相关的操作。这样,开发者能够更方便地进行现代OpenGL应用程序的开发。【感谢 chatgpt 3.5 帮忙 :)】,介绍得不错,确实就这么回事,之前还有个 GLU(OpenGL Utility Library),但相对来说较少使用,因为现代OpenGL更倾向于使用自定义的解决方案来实现类似的功能。
BTW, 还有个 GLEW
PROMPT:glew 与 glad 有什么不一样?
ANSWER:
GLEW(OpenGL Extension Wrangler Library)和GLAD(OpenGL Loader)是两个用于加载和管理OpenGL扩展函数的库,它们有以下不同之处:
1. 历史和使用范围:GLEW是比较早期的OpenGL扩展加载库,已经存在很长时间,并广泛用于许多项目。而GLAD是近年来出现的一个轻量级OpenGL扩展加载库,在性能和易用性方面进行了优化。
2. 生成方式:GLEW是使用C库的方式提供的,你需要下载和编译GLEW的源代码,然后将其连接到你的项目中。而GLAD是一个在线生成器,你可以使用它的在线服务来生成适用于你的OpenGL版本和扩展的自定义加载代码。
3. 用法和初始化:GLEW的用法相对来说更简单,只需要在初始化OpenGL上下文之后,调用`glewInit()`函数来初始化GLEW,并加载和绑定OpenGL的扩展函数。GLAD的使用方式也相对简单,你需要调用`gladLoadGL()`函数来加载OpenGL函数指针,并且确保在初始化OpenGL上下文后调用该函数。
4. 支持的OpenGL版本:GLEW支持的OpenGL版本比较广泛,并且提供了很好的向后兼容性,可以加载旧版本和最新版本的扩展函数。GLAD的生成器允许你选择特定的OpenGL版本和扩展,生成适用于你的需求的加载代码。
需要注意的是,无论是GLEW还是GLAD,它们的目的都是加载和管理OpenGL的扩展函数。你可以根据自己的需求选择其中之一来使用,它们在实际使用中并没有明显的性能优劣之分。选择合适的库主要取决于你对功能、使用习惯和项目要求的考虑。
画第一个三角形 Helloworld Triangle
开始之前,还是先把 GLFW 和 GLAD 给弄下来。
GLFW 搞起
https://www.glfw.org/download.html 我直接下载 64-bit Windows binaries,我偷懒直接使用(LearnOpengl.com 建议下源码自己构建,自己构建应该也比较简单,直接用 cmake build 就行了,后面有些例子如用于加载模形的 Assimp 万能库就必须要自己 build 了,因为可能与本地环境相关,比如 windows 支持 windows 特有 d3d等)。
GLAD 搞起
这个牛,直接在线自己构建 https://glad.dav1d.de/
我版本直接选了最高,如下:
万事具备,只欠东风啦。
我们的第一个 Opengl Helloworld 呼之欲出
1、把上面准备的依赖引入到我们的项目当中
2、先看看源码
当然也可以直接去看源版:
https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.2.hello_triangle_indexed/hello_triangle_indexed.cpp
源码分析:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 相当于头文件,先定义了下面用到的函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height); // 处理窗口大小变化事件
void processInput(GLFWwindow *window); // 处理键盘,鼠标等输入事件
// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// 不太优雅哈哈,后面会用单独文件加载进来,vs 与 fs 两文件
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";
int main()
{
// glfw: initialize and configure
// https://www.glfw.org/docs/latest/window.html#window_hints
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 我们用的 OpenGL version 3.3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 我们用的 OpenGL version 3.3
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 定义OpenGL模式(profile),使用的是核心模式。( core)
// 夸平台处理
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation, 创建窗口
// --------------------
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// 下面跟 webgl 差不多玩法去 处理 shader
// build and compile our shader program
// ------------------------------------
// vertex shader
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< infoLog << std::endl;
}
// fragment shader
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"
<< infoLog << std::endl;
}
// link shaders
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n"
<< infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = { // 三角形的三个顶点
-0.5f, -0.5f, +0.0f, // left
+0.5f, -0.5f, +0.0f, // right
+0.0f, +0.5f, +0.0f // top
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
glBindVertexArray(0);
// uncomment this call to draw in wireframe polygons.
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
// render loop
// 【这个有意思,web3d 走的是 requestAnimationFrame,也就是大概 1000ms / 60次 = 16ms/次, 间隔 16ms 调用一次】
// 这里呢?无时无刻可调用,谁电脑性能强谁就跑的FPS就强,牛哈!!!webgl webgpu 敢不敢这么玩?
while (!glfwWindowShouldClose(window))
{
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glDrawArrays(GL_TRIANGLES, 0, 3);
// glBindVertexArray(0); // no need to unbind it every time
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// c / c++ 好习惯,用完要清理释放内存
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
项目跑不起来?项目配置可参考上篇:《Opengl – 图形学 – C/C++ 每天进步一点点【子鼠】》
点“调试”跑起来了
再画 Helloworld Shadertoy【非官方例子,自己原创实现】
只画了个三角形当然没意思,前阵我沉迷过 Shadertoy 直达我的 Shadertoy,话说回来,先把 shadertoy 的 helloworld 实现一下如何?之前我已经用 Webgl(Glsl) / Webgpu(Wgsl) 实现过一次,回顾一下关键代码
Webgl(Glsl) 版 Shadertoy
// webgl.glsl.js
/*
uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iTime; // shader playback time (in seconds)
*/
const shaderToy = /* glsl */`
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord / iResolution.xy;
// Time varying pixel color
vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
// Output to screen
fragColor = vec4(col, 1.0);
}
`;
export const vs = /* glsl */`
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
`;
export const fs = /* glsl */ `
precision mediump float;
uniform vec2 iResolution;
uniform float iTime;
${shaderToy}
void main() {
vec4 fragColor = vec4(0, 0, 0, 1.0);
mainImage(fragColor, gl_FragCoord.xy);
gl_FragColor = fragColor;
}
`;
Webgpu(Wgsl) 版 Shadertoy
// shadertoy.wgsl.js
/*
uniform vec3 iResolution; // viewport resolution (in pixels)
uniform float iTime; // shader playback time (in seconds)
*/
export const vs = /* wgsl */`
@vertex
fn main(
@builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4<f32> {
var pos = array<vec2f, 6>(
vec2f( -1.0, -1.0), // left, bottom
vec2f( 1.0, -1.0), // right, bottom
vec2f( -1.0, 1.0), // left, top
// 2st triangle
vec2f( -1.0, 1.0), // left, top
vec2f( 1.0, -1.0), // right, bottom
vec2f( 1.0, 1.0), // right, top
);
return vec4<f32>(pos[vertexIndex], 0.0, 1.0);
}
`;
const shaderToy = /* wgsl */`
fn mainImage( fragCoord: vec2f, uni: Uniforms ) -> vec4f {
let iResolution = uni.iResolution;
let iTime = uni.iTime;
// Normalized pixel coordinates (from 0 to 1)
let uv = fragCoord / iResolution.xy;
// Time varying pixel color
let col = 0.5 + 0.5 * cos(iTime + vec3f(uv.xy, uv.x) + vec3f(0, 2, 4));
return vec4f(col, 1.0);
}
`;
export const fs = /* wgsl */ `
struct Uniforms {
iResolution: vec2f,
iTime: f32,
};
@group(0) @binding(0) var<uniform> uni: Uniforms;
${shaderToy}
@fragment
fn main(
@builtin(position) position: vec4f
) -> @location(0) vec4f {
var fragColor = vec4f(0, 0, 0, 1.0);
let fragCoord = position.xy;
fragColor = mainImage(fragCoord, uni);
return fragColor;
}
`;
思路都是差不多,就是把时间 iTime 与 屏幕分辨率(宽高)通过 uniform 传进 shader,然后所有的画图都在 frag shader 里面用 GPU 来画。
废话不多说,这就把 桌面客户端的 Helloworld Shadertoy 给办了。
C++ Opengl 版本的 Helloworld Shadertoy 的实现
1、引入 LearnOpengl.com 教程的 Shader 类
#include <learnopengl/shader_m.h>
这个类,只有一个 head,实现也全在 head 里面,注意它里面引入了 glm 图形计算的数学库类 #include <glm/glm.hpp>
,这个类如果是 web 端就类似 gl-matrix.js
这个数学库。
shader_m.h 这个类主要实现了 Shader Program 的常用功能,如加载与编译 shader vs 与 shader fs 文件,创建 shader Program 并把 VERTEX FRAGMENT PROGRAM 一起链接起来。
2、进入主题,主要代码
先看 c++ 代码
// main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <learnopengl/shader_m.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);
// settings
const unsigned int SCR_WIDTH = 1400;
const unsigned int SCR_HEIGHT = 1200;
// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;
int main()
{
// glfw: initialize and configure
// ------------------------------
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
// glfw window creation
// --------------------
GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Hello Shadertoy", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
// ---------------------------------------
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
// build and compile our shader program
// ------------------------------------
Shader ourShader("../src/shader.vs", "../src/shader.fs"); // you can name your shader files however you like
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
// float vertices[] = {
// // positions // colors
// 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
// -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
// 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top
// };
// 这里我们把上面的例子的画三角形,改为用画两三角形变成全屏的长方形
float vertices[] = {
// positions // colors
// 1st triangle
-1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // left, bottom
1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // right, bottom
-1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // left, top
// 2ed triangle
-1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // left, top
1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // right, bottom
1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, // right, top
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
// VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
// glBindVertexArray(0);
// shader configuration
// --------------------
// 这里把我们 shader 用到的 iTime, iResolution 作为 uniform 传入使用
ourShader.use();
ourShader.setFloat("iTime", 0.0f);
ourShader.setVec2("iResolution", (float)SCR_WIDTH, (float)SCR_HEIGHT);
// render loop
// -----------
while (!glfwWindowShouldClose(window))
{
// per-frame time logic
// --------------------
float currentFrame = static_cast<float>(glfwGetTime());
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
// input
// -----
processInput(window);
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// render the shadertoy helloworld
ourShader.use();
ourShader.setFloat("iTime", currentFrame); // 这里把我们 shader 用到的实时变化的 iTime 作为 uniform 传入使用
// ourShader.setVec2("iResolution", (float)SCR_WIDTH, (float)SCR_HEIGHT); // TODO: 监听窗口变化实时改变,这里先不实现,写死先
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 6); // 注意这里要用 6, 我们要一次画 6 个点,两个三角形组成一个四边形嘛
// glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
// -------------------------------------------------------------------------------
glfwSwapBuffers(window);
glfwPollEvents();
}
// optional: de-allocate all resources once they've outlived their purpose:
// ------------------------------------------------------------------------
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// glfw: terminate, clearing all previously allocated GLFW resources.
// ------------------------------------------------------------------
glfwTerminate();
return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
// make sure the viewport matches the new window dimensions; note that width and
// height will be significantly larger than specified on retina displays.
glViewport(0, 0, width, height);
}
再看 shader.vs 与 shader.fs 代码
// shader.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
}
// shader.fs
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
uniform float iTime;
uniform vec2 iResolution;
void main() {
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = gl_FragCoord.xy / iResolution.xy;
// Time varying pixel color
vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
FragColor = vec4(col, 1.0f);
}
项目目录结构如下:
用 cmake 调试,成了:
什么是 Shadertoy, 让 gpt 科普下:
Prompt: 介绍一下 Shadertoy
Answer:
Shadertoy是一个在线的交互式着色器编辑器和展示平台。它专注于图形编程和着色器开发领域,允许用户使用着色器语言编写和展示各种视觉效果。通过Shadertoy,用户可以在浏览器中创建复杂的图形效果和动画,并与全球的创作者社区分享和讨论他们的作品。
Shadertoy的核心是其着色器语言,它基于OpenGL Shading Language(GLSL),是一种用于编写图形着色器的高级语言。用户可以使用GLSL编写自定义的着色器代码,控制像素级别的图像处理和渲染。Shadertoy提供了一个用户友好的编辑器界面,使得编写和调试着色器代码变得更加容易。
在Shadertoy上,用户可以创建自己的着色器作品,调整各种参数和效果,实时预览结果,并与其他用户分享和讨论。平台上有一个活跃的创作者社区,可以浏览、搜索和评论其他人的作品。用户可以通过点赞和分享他们喜欢的作品,与其他创作者互动和学习。
除了在线编辑器和社区分享功能,Shadertoy还提供了一些额外的工具和特性,如音频分析和输入、相机和时间控制、材质库等,使得创作者能够更加丰富和个性化地表达他们的创意。
总的来说,Shadertoy是一个允许用户创建、分享和欣赏交互式着色器作品的平台,为图形编程和视觉效果的开发者提供了一个丰富的创作和学习空间。
【待续…】