threepp icon indicating copy to clipboard operation
threepp copied to clipboard

How to use the custom shader for drawing the height map(mesh) in threepp

Open asmwarrior opened this issue 4 months ago • 13 comments

Hi, here is class I used to draw a mesh

// =============================================================
// Helper Functions (outside the class)
// =============================================================

// Example height function
float f(float x, float y, float tx = 0.0f)
{
    float dx = x - 0.5f;
    float dy = y - 0.5f;
    float r = std::sqrt(dx * dx + dy * dy);
    return std::sin((r + tx) * 8.0f * 3.1415926f) * 0.5f;
}

// Generate grid like your old code
void generate_grid(int N, std::vector<threepp::Vector3>& vertices, std::vector<unsigned int>& indices)
{
    vertices.clear();
    indices.clear();

    for (int j = 0; j <= N; ++j)
    {
        for (int i = 0; i <= N; ++i)
        {
            float x = (float)i / (float)N;
            float y = (float)j / (float)N;
            float z = f(x, y);
            vertices.push_back({x, y, z});
        }
    }

    for (int j = 0; j < N; ++j)
    {
        for (int i = 0; i < N; ++i)
        {
            int row1 = j * (N + 1);
            int row2 = (j + 1) * (N + 1);

            // triangle 1
            indices.push_back(row1 + i);
            indices.push_back(row1 + i + 1);
            indices.push_back(row2 + i + 1);

            // triangle 2
            indices.push_back(row1 + i);
            indices.push_back(row2 + i + 1);
            indices.push_back(row2 + i);
        }
    }
}

///////////////////////////////////////////////////////////////////////////////

#define TEST 1

#if not TEST

class SurfaceRenderer
{
public:
    SurfaceRenderer()
    {
        m_Geometry = std::make_shared<threepp::BufferGeometry>();
        m_Material = threepp::ShaderMaterial::create();

        // Use the simplified shaders below
        m_Material->vertexShader = m_VertexShader;
        m_Material->fragmentShader = m_FragmentShader;

        // No need for ZL/ZH uniforms in the simplified version
        m_Material->uniforms = {};

        m_Mesh = std::make_shared<threepp::Mesh>(m_Geometry, m_Material);
        m_Mesh->frustumCulled = false;
    }

    void SetData(const std::vector<threepp::Vector3>& vertices,
                 const std::vector<unsigned int>& indices)
    {
        std::vector<float> verticesData;
        verticesData.reserve(vertices.size() * 3);
        for (const auto& v : vertices)
        {
            verticesData.push_back(v.x);
            verticesData.push_back(v.y);
            verticesData.push_back(v.z);
        }

        auto positionsUnique = threepp::TypedBufferAttribute<float>::create(verticesData, 3);
        m_Geometry->setAttribute("position", std::move(positionsUnique));
        m_Geometry->setIndex(indices);
        m_Geometry->computeVertexNormals();
        m_Geometry->computeBoundingSphere();
        m_Geometry->computeBoundingBox();
    }

    void SetZRange(float /*zl*/, float /*zh*/) {}

    std::shared_ptr<threepp::Mesh> GetMesh()
    {
        return m_Mesh;
    }

private:
    std::shared_ptr<threepp::BufferGeometry> m_Geometry;
    std::shared_ptr<threepp::ShaderMaterial> m_Material;
    std::shared_ptr<threepp::Mesh> m_Mesh;

    const std::string m_VertexShader = R"(#version 330 core
layout(location = 0) in vec3 vertPos;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

out vec3 pos;

void main()
{
    gl_Position = projectionMatrix * modelViewMatrix * vec4(vertPos, 1.0);
    pos = vertPos;
})";

    const std::string m_FragmentShader = R"(#version 330 core
uniform float ZL;
uniform float ZH;

in vec3 pos;
out vec3 color;

vec3 jet(float t)
{
    return clamp(vec3(1.5) - abs(4.0 * vec3(t) + vec3(-3, -2, -1)),
                 vec3(0), vec3(1));
}

void main()
{
    float param = (pos.z - ZL) / (ZH - ZL);
    color = jet(param);
})";
};

#else


// =============================================================
// TEST MODE: MeshBasicMaterial-based Renderer
// =============================================================
class SurfaceRenderer
{
public:

    SurfaceRenderer()
    {
        m_Geometry = std::make_shared<threepp::BufferGeometry>();
        m_Material = threepp::MeshBasicMaterial::create();
        m_Material->color = threepp::Color::red;
        m_Mesh = std::make_shared<threepp::Mesh>(m_Geometry, m_Material);
    }

    void SetData(const std::vector<threepp::Vector3>& vertices,
                 const std::vector<unsigned int>& indices)
    {
        std::vector<float> verticesData;
        verticesData.reserve(vertices.size() * 3);
        for (auto& v : vertices)
        {
            verticesData.push_back(v.x);
            verticesData.push_back(v.y);
            verticesData.push_back(v.z);
        }

        auto positionsUnique = threepp::TypedBufferAttribute<float>::create(verticesData, 3);
        m_Geometry->setAttribute("position", std::move(positionsUnique));
        m_Geometry->setIndex(indices);
        m_Geometry->computeVertexNormals();
        m_Geometry->computeBoundingSphere();
        m_Geometry->computeBoundingBox();
    }

    void SetZRange(float /*zl*/, float /*zh*/)
    {
        // ignored for testing
    }

    std::shared_ptr<threepp::Mesh> GetMesh()
    {
        return m_Mesh;
    }

private:

    std::shared_ptr<threepp::BufferGeometry> m_Geometry;
    std::shared_ptr<threepp::MeshBasicMaterial> m_Material;
    std::shared_ptr<threepp::Mesh> m_Mesh;
    const std::string m_VertexShader = "";
    const std::string m_FragmentShader = "";
};


#endif

Here is the client code to use this class:

        // Create and keep the instance alive
        surface = std::make_shared<SurfaceRenderer>();

        std::vector<threepp::Vector3> vertices1;
        std::vector<unsigned int> indices1;
        generate_grid(50, vertices1, indices1);

        surface->SetData(vertices1, indices1);

        float zl = +std::numeric_limits<float>::max();
        float zh = -std::numeric_limits<float>::max();
        for (auto &v: vertices1)
        {
            if(v.z < zl) zl = v.z;
            if(v.z > zh) zh = v.z;
        }
        surface->SetZRange(zl, zh);

        scene->add(surface->GetMesh());

You can see that if the TEST is defined as 1, then the simplified red mesh is shown, see the image shot below:

Image

But if the TEST is defined as 0, nothing is shown. I mean I can't use the ShaderMaterial class for customized shaders.

The expected output is like below, it is a kind of height map(jet color map), which means the higher parts of the mesh will shown in red, and the lower parts of the mesh will shown in blue, see image shot below:

Image

The above image is drawn by pure OpenGL code/render, I'm trying to migrate the code from the pure OpenGL to threepp framework, but found a bit hard. Can you help? Thanks.

BTW, It looks like the mouse rotation method has slightly different along different x,y,z axis. I mean in some axis' rotation, it is hard to rotate. Is the mouse rotation follow the way like: Object Mouse Trackball - OpenGL Wiki?

In my own pure OpenGL code, I don't see the mouse rotation is different if I rotate on different direction. But under threepp, it looks like in some axis, the rotation angle has a positive and negative limit? Thanks.

asmwarrior avatar Sep 02 '25 08:09 asmwarrior

For the colormap, could you use LUT? https://github.com/markaren/threepp/blob/master/examples/misc/lut.cpp

markaren avatar Sep 02 '25 09:09 markaren

For the colormap, could you use LUT? https://github.com/markaren/threepp/blob/master/examples/misc/lut.cpp

Thanks, you mean look up table for the color selection? I think that code does not touch the shader code.

But for me, I need some customized shader code, it will make the scene much flexible. I can tweak the shader code, and generate many different kinds of color effects. And the shader code runs much faster, because it runs on GPU.

asmwarrior avatar Sep 02 '25 09:09 asmwarrior

I'm not sure if the threepp library has some kinds of "shader debug log" information I can use?

I mean I still has the issue, but I don't know how to debug it, whether it is a shader code error or other error, the code builds OK, but it just shows nothing when I run it.

Thanks.

asmwarrior avatar Sep 04 '25 06:09 asmwarrior

renderer.checkShaderErrors = true; might produce output, but I'm not completely sure.

markaren avatar Sep 04 '25 09:09 markaren

renderer.checkShaderErrors = true; might produce output, but I'm not completely sure.

OK, thanks.

When enabled this option, I see one log message:

[Shader error] Attached vertex shader is not compiled.

So, I will check whether the vertex shader code has some issue.

asmwarrior avatar Sep 05 '25 01:09 asmwarrior

OK, now, I see that I find the reason.

If you looked at the source code here:

https://github.com/markaren/threepp/blob/ada259b72435e380c8e3def429fc8001233b1a8f/src/threepp/renderers/gl/GLProgram.cpp#L718

I see there are many shader code prefix string which is defined as std::string prefixVertex, prefixFragment;

Which means unless I set the value isRawShaderMaterial = true. All my code will append to the prefix string.

Now, which the code changes here:

// Corrected Vertex Shader: No #version at the top
const std::string m_VertexShader = R"(
out vec3 pos;

void main()
{
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    pos = position;
}
)";

const std::string m_FragmentShader = R"(
uniform float ZL;
uniform float ZH;

in vec3 pos;
out vec4 color;

vec3 jet(float t)
{
    return clamp(vec3(1.5) - abs(4.0 * vec3(t) + vec3(-3, -2, -1)),
                 vec3(0), vec3(1));
}

void main()
{
    float param = (pos.z - ZL) / (ZH - ZL);
    vec3 c = jet(param);
    color = vec4(c, 1.0);
}
)";

And here is the result image:

Image

asmwarrior avatar Sep 05 '25 02:09 asmwarrior

The shader issue is fixed now, but I'm not sure how to fix the mouse rotation issue mentioned in the first post. In some axis rotation, the angle get locked or limited.

asmwarrior avatar Sep 05 '25 13:09 asmwarrior

    class OrbitControls {

    public:
        bool enabled = true;

        Vector3 target;

        float minDistance = 0.f;
        float maxDistance = std::numeric_limits<float>::infinity();

        float minZoom = 0.f;
        float maxZoom = std::numeric_limits<float>::infinity();

        float minPolarAngle = 0.f;
        float maxPolarAngle = math::PI;

        // How far you can orbit horizontally, upper and lower limits.
        // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
        float minAzimuthAngle = -std::numeric_limits<float>::infinity();// radians
        float maxAzimuthAngle = std::numeric_limits<float>::infinity(); // radians

Maybe, I need to set the PolarAngle to infinity to unlock the limit.

asmwarrior avatar Sep 05 '25 14:09 asmwarrior

https://threejs.org/docs/#examples/en/controls/TrackballControls

I think what I need is a Trackball control.

While when I looked at the source code of threepp here:

threepp/include/threepp/controls at master · markaren/threepp

I see it does not have the trackball control?

Thanks.

asmwarrior avatar Sep 05 '25 14:09 asmwarrior

I see it does not have the trackball control?

Correct. https://github.com/mrdoob/three.js/blob/r129/examples/jsm/controls/TrackballControls.js is not implemented.

markaren avatar Sep 06 '25 19:09 markaren

I have create another issue for the trackball control implementation request, thanks.

asmwarrior avatar Sep 11 '25 03:09 asmwarrior

There are several class of the material: RawShaderMaterial and ShaderMaterial, if I use the former one, the full shader code is needed. If the later one is used, only portion of the shader code is needed. Some of the uniform variables and other codes were already implemented by the threepp framework. I'm not sure what's the advantage of using the later one, maybe it will affect the raytracking feature or other features? Any ideas? Thanks.

asmwarrior avatar Sep 11 '25 07:09 asmwarrior

It should work the same way as in three.js. I don't really have the full details.

markaren avatar Sep 11 '25 14:09 markaren