This is one of my early projects.

Motivation & Goal

The goal of this project was to achieve a non-photorealistic rendering effect using feature line detection and attribute-based color mapping.

Instead of post-processing edge detection, this project uses an edge buffer to detect silhouettes, and a threshold on the angle between two front-facing polygons to detect crease edges.

Features

Feature Lines (Silhouette & Crease)

Instead of post processing edge detection, the program using edge buffer to detect silhouette, and a threshold of angle between two front-facing polygons to detect crease edges.

silhouette

//adjacent list
struct Node {
    unsigned int v;                     //vertex id(index)
    unsigned int f = 0;                 //front face bit
    unsigned int b = 0;                 //back face bit
    std::vector<glm::vec3> norms;       //normals of 2 faces
};

// detect silhouette
for (int j = 0; j < edgeBuffer.size(); ++j) {
    Vertex v0, v1;
    for (auto it = edgeBuffer[j].begin(); it != edgeBuffer[j].end(); ++it) {
        //if front bit and back bit are both 1, which means it is a silhooute
        if ((*it).b && (*it).f) {
            v0 = ourModel.meshes[0].vertices[j];
            v1 = ourModel.meshes[0].vertices[(*it).v];
            //cout << j << " "  << (*it).v << " " << (*it).b << " " << (*it).f << endl;
            vertices.push_back(v0.Position);
            vertices.push_back(v1.Position);
        }
    }
}

// detect crease
for (int j = 0; j < edgeBuffer.size(); ++j) {
    Vertex v0, v1;
    for (auto it = edgeBuffer[j].begin(); it != edgeBuffer[j].end(); ++it) {
        //if it's front face
        if (!(*it).f && !(*it).b) {
            v0 = ourModel.meshes[0].vertices[j];
            v1 = ourModel.meshes[0].vertices[(*it).v];
            if (it->norms.size() >= 2) {
                glm::vec3 norm1 = it->norms[0];
                glm::vec3 norm2 = it->norms[1];
                if (glm::dot(norm1, norm2) <= glm::cos(glm::radians(180.f - creaseAngle))) {
                    vertices.push_back(v0.Position);
                    vertices.push_back(v1.Position);
                }
            }
        }
    }
}

Texture

Based on RGB to HSV conversion, I implemented shader functions to convert between RGB and HSV color spaces for hue-based color manipulation.

vec3 rgbTohsv(vec3 c)vec3 rgbTohsv(vec3 c)
{
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));

    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsvTorgb(vec3 c)
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
  

In real-world lighting, surface color appearance changes depending on cool skylight and warm sunlight contributions.

I applied this principle to introduce subtle hue variation across surfaces, simulating real-world lighting shifts.

Hue Changes Example

The system applies the user-selected base color onto a grayscale texture, then adjusts the hue to create a natural-looking stylized effect.

Gallary

In the end, the renderer achieved a pleasing non-photorealistic effect, as shown below:

Screenshots of the program.

Updated: