STATUS: NOMINAL LOCAL TIME: 00:00:00 返回园内

非真实感渲染

非真实感渲染

使用Morer-Engine,main分支下
参考角色

首先导入模型,查看albedo:

为了实现NPR,先给Moer-Engine添加新的ShadingMode,并配置UI:

// SharedEnum.h
EnumParam(EShadingMode, DEFAULT_PBR, DEBUG, NPR);

// RasterConfig.h
// EnumParam(EShadingMode, DEFAULT, DEBUG);
static const UnorderedMap<EShadingMode, std::string> s_shading_mode_name_map = {
    {EShadingMode::DEFAULT_PBR, "Default PBR (GAMES202)"},
    {EShadingMode::DEBUG, "Debug"},
    {EShadingMode::NPR, "NPR (Non-Photorealistic)"},
};

二分色阶着色

cel shading

我们用亮部暗部和一个阈值来替代Ramp贴图,在数学意义上是一个[0,1]上的函数,法线向量与光源方向向量的点积NdotL范围是[-1,1],因此我们直接clamp到[0,1]上作为输入coeff

float3 L = -lighting_data.main_light_direction;
float nDotL = dot(N, L);
float coeff = saturate(nDotL);

通过比较输入与阈值threshold的大小来确定输出,这里smoothness可以让边界变得更软:

float t = smoothstep(threshold - smoothness, threshold + smoothness, coeff); // sigmoid函数,中间是0到1的平滑过渡

最后根据亮部和暗部以及albedo获取最终颜色:

float3 lit_color = albedo * param.npr_lit_intensity;
float3 shadow_color = albedo * param.npr_shadow_color;
float3 color = lerp(shadow_color, lit_color, t);

half lambert

直接使用saturate(nDotL)的版本中,无论怎么调threshold,背光面永远是暗部,因此我们考虑使用半兰伯特(half lambert):lightingCoefficient = 0.5*nDotL + 0.5

float  coeff = param.npr_use_half_lambert ? (0.5 * nDotL + 0.5) : saturate(nDotL);

这样把整个球面都映射到 [0,1],给threshold提供了完整的调节空间

其他

目前我们只考虑了单个光源,一般在PBR中,会将多个光源的效果累加,在NBR中,我们选择只取主光源来避免多光源合并的问题。

在最后额外添加环境光ambient来提亮暗部,最终代码如下:

if (param.shading_mode == Moer::EShadingMode::NPR) {
    // 2-Tone NPR: Half-Lambert + smoothstep threshold
    float3 V = normalize(lighting_data.camera_position - position.xyz);

    // 主方向光作为 key light
    float3 L = -lighting_data.main_light_direction;
    float nDotL = dot(N, L);
    float coeff = param.npr_use_half_lambert ? (0.5 * nDotL + 0.5) : saturate(nDotL);
    float t = smoothstep(param.npr_threshold - param.npr_smoothness,
        param.npr_threshold + param.npr_smoothness, coeff);

    float3 lit_color = albedo * param.npr_lit_intensity;
    float3 shadow_color = albedo * param.npr_shadow_color;
    float3 color = lerp(shadow_color, lit_color, t);

    if (param.enable_extra_ambient) {
        color += param.extra_ambient_intensity * param.extra_ambient_color * albedo;
    }
    return float4(max(color, 0.0), 1.0);
}

效果图:

参考文章

描边

采用边缘检测,使用Sobel算子对屏幕像素进行卷积。首先为MoerEngine添加一个OutlinePass。
这里使用3×3的矩阵,每个像素要看周围8个像素的采样。

static const float2 s_offsets[8] = {
    float2(-1, -1), float2(0, -1), float2(1, -1),
    float2(-1,  0),                float2(1,  0),
    float2(-1,  1), float2(0,  1), float2(1,  1)
};
float4 main(float2 uv : TEXCOORD0) : SV_TARGET {
	...
	// 当前采样点:深度和法线
    float  center_depth   = TextureHandle(param.depth_tex).Sample2D<float>(uv);
    float3 center_normal  = TextureHandle(param.normal_tex).Sample2D<float3>(uv).rgb;
    float3 N_center       = normalize(center_normal * 2.0 - 1.0);

    float  depth_max_diff  = 0.0;
    float  normal_max_diff = 0.0;
    float2 step_size       = param.inv_resolution * param.outline_thickness;

    
    for (int i = 0; i < 8; i++) {
        float2 sample_uv = uv + s_offsets[i] * step_size;

        float  sample_depth  = TextureHandle(param.depth_tex).Sample2D<float>(sample_uv);
        float3 sample_normal = TextureHandle(param.normal_tex).Sample2D<float3>(sample_uv).rgb;

        float depth_scale = max(abs(center_depth), 1e-5);
        float depth_diff  = abs(center_depth - sample_depth) / depth_scale;
        depth_max_diff    = max(depth_max_diff, depth_diff);

        float3 N_sample   = normalize(sample_normal * 2.0 - 1.0);
        float  normal_diff = 1.0 - abs(dot(N_center, N_sample));
        normal_max_diff    = max(normal_max_diff, normal_diff);
    }
	// 判定边缘,由参数确定阈值
    bool is_edge = (depth_max_diff > param.edge_depth_threshold)
                || (normal_max_diff > param.edge_normal_threshold);
}

效果图:


参考

像素风格

实现像素化画面的shader核心如下

// input uv
uv = uv * param.pixel_count;
uv = floor(uv);
uv = uv / param.pixel_count // 回到[0, 1]
}

效果就是pixel_count会讲屏幕划分成水平pixel_count、数值pixel_count个像素格,原本在改格内的不同像素此时都取得相同颜色;pixel_count越小,像素越大,反之越接近原画面。
color_quant_bits则对颜色值进行离散化处理。

其他

其实还有许多可以实现的东西

posted @ 2026-06-18 21:09  猫爹爱猫娘  阅读(2)  评论(0)    收藏  举报