屏幕空间反射SSR-笔记

屏幕空间反射SSR

相关文章:
[OpenGL] 屏幕空间反射效果

Games202-RealTime GI in Screen Space

github上的例子,使用visual studio2019
github例子对应的文章



使用OpenGL和C++实现发光柱子的SSR倒影

下面是一个使用OpenGL和C++实现屏幕空间反射(SSR)来创建发光柱子倒影的完整示例。这个实现包括基本的SSR技术、发光效果和倒影渲染。

1. 基本框架

首先,我们需要设置OpenGL环境并创建必要的着色器。

cpp 复制代码
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
#include <vector>
#include "shader.h" // 自定义着色器加载类

// 窗口尺寸
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// 全局变量
GLuint gBuffer, gPosition, gNormal, gAlbedoSpec;
GLuint quadVAO, quadVBO;
GLuint cubeVAO, cubeVBO;
GLuint floorVAO, floorVBO;

// 函数声明
void renderScene(Shader &shader);
void renderQuad();
void renderCube();
void setupGBuffer();
void setupScreenQuad();
void setupGeometry();

2. 初始化GBuffer

SSR需要几何缓冲区(GBuffer)来存储位置、法线和材质信息。

cpp 复制代码
void setupGBuffer() {
    glGenFramebuffers(1, &gBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    
    // 位置颜色缓冲
    glGenTextures(1, &gPosition);
    glBindTexture(GL_TEXTURE_2D, gPosition);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);
    
    // 法线颜色缓冲
    glGenTextures(1, &gNormal);
    glBindTexture(GL_TEXTURE_2D, gNormal);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
    
    // 颜色 + 镜面颜色缓冲
    glGenTextures(1, &gAlbedoSpec);
    glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0);
    
    // 告诉OpenGL我们要使用哪些颜色附件进行渲染
    GLuint attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
    glDrawBuffers(3, attachments);
    
    // 创建并附加深度缓冲
    GLuint rboDepth;
    glGenRenderbuffers(1, &rboDepth);
    glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
    
    // 检查完整性
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "Framebuffer not complete!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

3. 几何体设置

创建柱子和地板的几何体。

cpp 复制代码
void setupGeometry() {
    // 柱子 (圆柱体)
    std::vector<float> vertices;
    const float radius = 0.5f;
    const float height = 2.0f;
    const int segments = 32;
    
    // 侧面
    for (int i = 0; i <= segments; ++i) {
        float angle = 2.0f * M_PI * float(i) / float(segments);
        float x = radius * cosf(angle);
        float z = radius * sinf(angle);
        
        // 底部顶点
        vertices.push_back(x);
        vertices.push_back(0.0f);
        vertices.push_back(z);
        vertices.push_back(x);
        vertices.push_back(0.0f);
        vertices.push_back(z);
        vertices.push_back(0.0f);
        vertices.push_back(1.0f);
        vertices.push_back(0.0f);
        
        // 顶部顶点
        vertices.push_back(x);
        vertices.push_back(height);
        vertices.push_back(z);
        vertices.push_back(x);
        vertices.push_back(0.0f);
        vertices.push_back(z);
        vertices.push_back(0.0f);
        vertices.push_back(1.0f);
        vertices.push_back(0.0f);
    }
    
    glGenVertexArrays(1, &cubeVAO);
    glGenBuffers(1, &cubeVBO);
    glBindVertexArray(cubeVAO);
    glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
    glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), &vertices[0], GL_STATIC_DRAW);
    
    // 位置属性
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
    // 法线属性
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float)));
    // 切线属性
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6 * sizeof(float)));
    
    // 地板 (平面)
    float floorVertices[] = {
        // 位置             // 法线         // 切线
        -5.0f, 0.0f, -5.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
         5.0f, 0.0f, -5.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
         5.0f, 0.0f,  5.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f,
        -5.0f, 0.0f,  5.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f
    };
    unsigned int floorIndices[] = {
        0, 1, 2,
        0, 2, 3
    };
    
    glGenVertexArrays(1, &floorVAO);
    glGenBuffers(1, &floorVBO);
    GLuint floorEBO;
    glGenBuffers(1, &floorEBO);
    
    glBindVertexArray(floorVAO);
    glBindBuffer(GL_ARRAY_BUFFER, floorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(floorVertices), floorVertices, GL_STATIC_DRAW);
    
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, floorEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(floorIndices), floorIndices, GL_STATIC_DRAW);
    
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(float), (void*)(6 * sizeof(float)));
    
    glBindVertexArray(0);
}

4. 着色器

我们需要几个着色器:几何通道着色器、光照着色器和SSR着色器。

几何通道着色器 (geometry.vs 和 geometry.fs)

glsl 复制代码
// geometry.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec3 aTangent;

out VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec3 Tangent;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main() {
    vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
    vs_out.Normal = mat3(transpose(inverse(model))) * aNormal;
    vs_out.Tangent = mat3(model) * aTangent;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
glsl 复制代码
// geometry.fs
#version 330 core
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

in VS_OUT {
    vec3 FragPos;
    vec3 Normal;
    vec3 Tangent;
} fs_in;

uniform vec3 color;
uniform float specular;
uniform float emissive;

void main() {
    // 存储片段位置向量到G缓冲
    gPosition = fs_in.FragPos;
    // 存储法线向量到G缓冲
    gNormal = normalize(fs_in.Normal);
    // 存储漫反射颜色
    gAlbedoSpec.rgb = color;
    // 存储镜面强度
    gAlbedoSpec.a = specular;
    
    // 如果是发光物体,增加自发光
    if (emissive > 0.0) {
        gAlbedoSpec.rgb += color * emissive;
    }
}

SSR着色器 (ssr.vs 和 ssr.fs)

glsl 复制代码
// ssr.vs
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main() {
    TexCoords = aTexCoords;
    gl_Position = vec4(aPos, 0.0, 1.0);
}
glsl 复制代码
// ssr.fs
#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;
uniform sampler2D sceneTexture;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 invView;
uniform mat4 invProjection;

uniform vec3 cameraPos;

const float maxRayDistance = 100.0;
const int maxSteps = 128;
const float stride = 1.0;
const float binarySearchSteps = 8;
const float rayHitThreshold = 0.01;

vec3 binarySearch(inout vec3 dir, inout vec3 hitCoord, inout float dDepth);
vec4 rayCast(vec3 dir, inout vec3 hitCoord, out bool success);
vec3 hash(vec3 a);

void main() {
    // 获取输入数据
    vec3 FragPos = texture(gPosition, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec, TexCoords).a;
    
    // 计算反射向量
    vec3 viewDir = normalize(FragPos - cameraPos);
    vec3 reflectDir = reflect(viewDir, Normal);
    
    // 初始化射线
    vec3 hitPos = FragPos;
    bool success = false;
    vec4 coords = rayCast(reflectDir, hitPos, success);
    
    vec3 SSR = vec3(0.0);
    if (success) {
        SSR = texture(sceneTexture, coords.xy).rgb;
        // 根据距离衰减
        float fade = 1.0 - smoothstep(0.0, 1.0, length(FragPos - hitPos) / maxRayDistance);
        SSR *= fade * Specular;
    }
    
    // 混合原始颜色和SSR
    FragColor = vec4(SSR + Albedo, 1.0);
}

vec3 binarySearch(inout vec3 dir, inout vec3 hitCoord, inout float dDepth) {
    float depth;
    vec4 projectedCoord;
    
    for (int i = 0; i < binarySearchSteps; i++) {
        projectedCoord = projection * view * vec4(hitCoord, 1.0);
        projectedCoord.xy /= projectedCoord.w;
        projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
        
        depth = texture(gPosition, projectedCoord.xy).z;
        dDepth = hitCoord.z - depth;
        
        dir *= 0.5;
        if (dDepth > 0.0)
            hitCoord += dir;
        else
            hitCoord -= dir;
    }
    
    projectedCoord = projection * view * vec4(hitCoord, 1.0);
    projectedCoord.xy /= projectedCoord.w;
    projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
    
    return vec3(projectedCoord.xy, depth);
}

vec4 rayCast(vec3 dir, inout vec3 hitCoord, out bool success) {
    dir *= stride;
    
    float depth;
    int steps;
    vec4 projectedCoord;
    float dDepth;
    
    for (steps = 0; steps < maxSteps; steps++) {
        hitCoord += dir;
        
        projectedCoord = projection * view * vec4(hitCoord, 1.0);
        projectedCoord.xy /= projectedCoord.w;
        projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
        
        // 检查是否在屏幕外
        if (projectedCoord.x < 0.0 || projectedCoord.x > 1.0 || 
            projectedCoord.y < 0.0 || projectedCoord.y > 1.0) {
            success = false;
            return vec4(0.0);
        }
        
        depth = texture(gPosition, projectedCoord.xy).z;
        dDepth = hitCoord.z - depth;
        
        if (dir.z - dDepth < 1.2) {
            if (dDepth <= 0.0) {
                vec3 Result = binarySearch(dir, hitCoord, dDepth);
                
                if (dDepth < rayHitThreshold && Result.z - hitCoord.z < rayHitThreshold) {
                    success = true;
                    return vec4(Result, 1.0);
                }
                break;
            }
        }
    }
    
    success = false;
    return vec4(0.0);
}

vec3 hash(vec3 a) {
    a = fract(a * 0.8);
    a += dot(a, a.xyz + 19.19);
    return fract((a.xxy + a.yxx) * a.zyx);
}

5. 渲染循环

cpp 复制代码
int main() {
    // 初始化GLFW和OpenGL
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "SSR Glowing Pillar", NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    
    glewExperimental = GL_TRUE;
    if (glewInit() != GLEW_OK) {
        std::cout << "Failed to initialize GLEW" << std::endl;
        return -1;
    }
    
    // 配置全局OpenGL状态
    glEnable(GL_DEPTH_TEST);
    
    // 构建和编译着色器
    Shader geometryShader("geometry.vs", "geometry.fs");
    Shader ssrShader("ssr.vs", "ssr.fs");

-----------------------------------------------------------------------
# 屏幕空间反射(SSR)着色器详细解释

`ssr.fs`(屏幕空间反射片段着色器)是实现屏幕空间反射效果的核心部分。下面我将详细解释这个着色器的每个关键部分。

## 1. 输入输出和统一变量

```glsl
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D gPosition;  // 位置纹理(GBuffer)
uniform sampler2D gNormal;    // 法线纹理(GBuffer)
uniform sampler2D gAlbedoSpec;// 颜色和镜面强度纹理(GBuffer)
uniform sampler2D sceneTexture; // 场景颜色纹理

uniform mat4 projection;      // 投影矩阵
uniform mat4 view;            // 视图矩阵
uniform mat4 invView;         // 视图逆矩阵
uniform mat4 invProjection;   // 投影逆矩阵

uniform vec3 cameraPos;       // 相机位置

这些变量提供了SSR计算所需的所有输入数据:

  • gPosition, gNormal, gAlbedoSpec来自几何缓冲区(GBuffer)
  • sceneTexture是已经渲染的场景颜色
  • 矩阵用于空间转换
  • cameraPos用于计算视线方向

2. 常量定义

glsl 复制代码
const float maxRayDistance = 100.0;  // 最大射线距离
const int maxSteps = 128;            // 最大步进次数
const float stride = 1.0;            // 射线步进步长
const float binarySearchSteps = 8;   // 二分查找步数
const float rayHitThreshold = 0.01;  // 射线命中阈值

这些常量控制SSR算法的质量和性能:

  • maxRayDistance限制反射射线的最大长度
  • maxSteps限制射线步进的最大次数
  • stride控制每一步的步长
  • binarySearchSteps控制精确命中时的二分查找精度
  • rayHitThreshold定义何时认为射线命中表面

3. 主函数(main)

glsl 复制代码
void main() {
    // 获取输入数据
    vec3 FragPos = texture(gPosition, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec, TexCoords).a;
    
    // 计算反射向量
    vec3 viewDir = normalize(FragPos - cameraPos);
    vec3 reflectDir = reflect(viewDir, Normal);
    
    // 初始化射线
    vec3 hitPos = FragPos;
    bool success = false;
    vec4 coords = rayCast(reflectDir, hitPos, success);
    
    vec3 SSR = vec3(0.0);
    if (success) {
        SSR = texture(sceneTexture, coords.xy).rgb;
        // 根据距离衰减
        float fade = 1.0 - smoothstep(0.0, 1.0, length(FragPos - hitPos) / maxRayDistance);
        SSR *= fade * Specular;
    }
    
    // 混合原始颜色和SSR
    FragColor = vec4(SSR + Albedo, 1.0);
}

主函数流程:

  1. 从GBuffer获取当前像素的位置、法线、颜色和镜面强度
  2. 计算从相机到当前像素的视线方向(viewDir)
  3. 使用GLSL内置的reflect函数计算反射方向
  4. 调用rayCast函数进行射线步进,寻找反射点
  5. 如果找到反射点,从场景纹理中采样反射颜色
  6. 根据距离和镜面强度调整反射强度
  7. 将反射颜色与原始颜色混合输出

4. 射线步进(rayCast)

glsl 复制代码
vec4 rayCast(vec3 dir, inout vec3 hitCoord, out bool success) {
    dir *= stride;
    
    float depth;
    int steps;
    vec4 projectedCoord;
    float dDepth;
    
    for (steps = 0; steps < maxSteps; steps++) {
        hitCoord += dir;
        
        projectedCoord = projection * view * vec4(hitCoord, 1.0);
        projectedCoord.xy /= projectedCoord.w;
        projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
        
        // 检查是否在屏幕外
        if (projectedCoord.x < 0.0 || projectedCoord.x > 1.0 || 
            projectedCoord.y < 0.0 || projectedCoord.y > 1.0) {
            success = false;
            return vec4(0.0);
        }
        
        depth = texture(gPosition, projectedCoord.xy).z;
        dDepth = hitCoord.z - depth;
        
        if (dir.z - dDepth < 1.2) {
            if (dDepth <= 0.0) {
                vec3 Result = binarySearch(dir, hitCoord, dDepth);
                
                if (dDepth < rayHitThreshold && Result.z - hitCoord.z < rayHitThreshold) {
                    success = true;
                    return vec4(Result, 1.0);
                }
                break;
            }
        }
    }
    
    success = false;
    return vec4(0.0);
}

射线步进流程:

  1. 沿着反射方向(dir)逐步前进
  2. 每一步将世界坐标转换为屏幕坐标(projectedCoord)
  3. 检查是否超出屏幕边界
  4. 从GBuffer获取当前位置的深度值
  5. 比较当前射线深度与场景深度(dDepth)
  6. 当射线进入表面下方时(dDepth <= 0.0),进入二分查找阶段
  7. 如果找到足够接近的命中点,返回命中坐标

5. 二分查找(binarySearch)

glsl 复制代码
vec3 binarySearch(inout vec3 dir, inout vec3 hitCoord, inout float dDepth) {
    float depth;
    vec4 projectedCoord;
    
    for (int i = 0; i < binarySearchSteps; i++) {
        projectedCoord = projection * view * vec4(hitCoord, 1.0);
        projectedCoord.xy /= projectedCoord.w;
        projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
        
        depth = texture(gPosition, projectedCoord.xy).z;
        dDepth = hitCoord.z - depth;
        
        dir *= 0.5;
        if (dDepth > 0.0)
            hitCoord += dir;
        else
            hitCoord -= dir;
    }
    
    projectedCoord = projection * view * vec4(hitCoord, 1.0);
    projectedCoord.xy /= projectedCoord.w;
    projectedCoord.xy = projectedCoord.xy * 0.5 + 0.5;
    
    return vec3(projectedCoord.xy, depth);
}

二分查找流程:

  1. 在粗略命中点附近进行精确查找
  2. 每一步将搜索方向减半(dir *= 0.5)
  3. 根据深度差调整搜索方向
  4. 最终返回精确的屏幕坐标和深度值

6. 关键算法细节

  1. 屏幕空间限制:SSR只能在屏幕可见的范围内工作,无法反射屏幕外的物体。

  2. 射线步进策略

    • 先用大步长快速遍历
    • 发现潜在命中点时切换到二分查找精确确定位置
  3. 深度比较:通过比较射线深度和场景深度确定是否命中表面。

  4. 衰减处理:反射强度随距离衰减,避免远处反射过于明显。

  5. 镜面控制:使用材质的镜面系数控制反射强度,只有高光材质才有强烈反射。

7. 优化考虑

  1. 早期终止:当射线超出屏幕或达到最大距离时提前终止。

  2. 自适应步长:可以根据场景复杂度动态调整步长。

  3. 模糊处理:可以对反射结果进行适当模糊以减少噪点。

  4. 边缘淡化:在屏幕边缘淡化反射效果,掩盖SSR的局限性。

这个SSR实现提供了实时反射效果,特别适合光滑表面和发光物体的倒影渲染,如问题中要求的发光柱子效果。

相关推荐
唯之为之3 个月前
# Vue3.5常用特性整理
vue3·ssr
dlnu20152506224 个月前
ssr实现方案
前端·javascript·ssr
JerryXZR4 个月前
Vue SSR基础介绍与实践
前端·vue.js·ssr·vue ssr
余生H4 个月前
Angular v19 (三):增量水合特性详解 - 什么是水合过程?有哪些应用场景?与 Qwik 相比谁更胜一筹?- 哪个技术好我就学哪个,这就是吸心大法吧
前端·javascript·angular·ssr·前端优化·qwik
Amd7945 个月前
Nuxt.js 应用中的 render:response 事件钩子
nuxt.js·事件·ssr·处理·钩子·修改·响应
Amd7945 个月前
Nuxt.js 应用中的 dev:ssr-logs 事件钩子
开发·监控·日志·nuxt.js·调试·ssr·钩子
努力挣钱的小鑫6 个月前
Nuxt3 SSR 服务端渲染部署 PM2 全流程(Nest.js 同理)
开发语言·javascript·ecmascript·ssr·ssr部署
Amd7946 个月前
Nuxt.js 应用中的 app:redirected 钩子详解
认证·日志·nuxt.js·ssr·重定向·钩子·示例
Amd7947 个月前
Nuxt Kit 中的上下文处理
vue·框架·nuxt·模块化·ssr·ssg·上下文