How do I create a 3d circle text?

I have created a text box using TextNode which displays changing coordinates of models. It looks like this:
image

Is there some method of creating similar box but with circular coordinates? Something like this:
image
Maybe some node warping on my flat text box? I need to display live text so something using TextNode will be optimal?
I tried modifying geom vertex data of text box but all the modifications only affect the background card and not the text itself. Another option would be to use live textures on hollow sphere but that seems unnecessary complicated. What I am looking for is some method to ‘fold’ my planar text box into a cylinder

Hmm… Two thoughts:

First is that I think that the actual text has geometry, too. As a result, it might be possible to use a vertex-shader to warp that geometry into a cylindrical shape.

And second is that you could perhaps render your text to a texture and then apply that texture to a cylinder.

However, there may be better ways of doing what you have in mind, so I recommend waiting and seeing what others perhaps suggest!

Thanks for the reply. I will look into your ideas and if anything works or someone does not give a better solution I will post my solution here.

1 Like

Interesting problem. Modifying the geometry data yourself is an option (though you have to get the right geom) or using a shader would also work.

Another option is to get text system to assemble the text as a single texture, after which you can apply it to a mesh that is appropriately UV-mapped. The PNMTextMaker class might be the easiest way to do this, it lets you draw text into a PNMImage that you can then store into a texture.

1 Like

I did end up following very similar route to the one you suggest but I used the PIL library to draw text on texture and made a custom hollow cylinder procedurally in Panda3d. The problem with this approach is that texture update is somewhat computationally heavy and I couldn’t display real stream of data while maintaining 60 fps. I decided on a compromise as I was under heavy time constraint and added a delay in texture update. Results look OK.
On geom node approach I couldn’t find the geom node of the displayed text no matter what I tried. Geom node would have been the simplest solution for me because I don’t understand shaders very well.

1 Like

Here is an attempt to use a shader that warps text of a TextNode’s internal GeomNode to fit along a circle:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase


VERT_SHADER = """
    #version 130

    uniform mat4 p3d_ModelViewProjectionMatrix;
    uniform mat4 p3d_TextureMatrix;
    in vec4 p3d_Vertex;
    in vec4 p3d_Color;
    in vec2 p3d_MultiTexCoord0;

    out vec2 v_texcoord;
    out vec4 v_color;

    void main() {
        vec4 pos;
        float radius = 10.;
        float angle, x, y;
        // the angle of the vertex depends on its x-coordinate as well as the
        // radius of the circle to warp the text along
        angle = atan(p3d_Vertex.x, radius);
        x = sin(angle) * radius;
        y = -cos(angle) * radius;
        pos = vec4(x, y, p3d_Vertex.z, 1.);
        gl_Position = p3d_ModelViewProjectionMatrix * pos;
        v_texcoord = (p3d_TextureMatrix * vec4(p3d_MultiTexCoord0, 0, 1)).xy;
        v_color = p3d_Color;
    }
"""

FRAG_SHADER = """
    #version 130

    uniform sampler2D p3d_Texture0;

    in vec2 v_texcoord;
    in vec4 v_color;

    out vec4 color;

    void main() {
        color = v_color * texture2D(p3d_Texture0, v_texcoord);
    }
"""


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.disable_mouse()
        self.camera.set_pos(0., -30., 25.)
        self.camera.look_at(0., 0., 0.)
        text_node = TextNode("text")
        text_node.set_text("This is some curved text, warped to fit around a circle.")
        text_node.set_align(TextNode.A_center)
        text_node.set_slant(0.3)
        text_node.set_text_color(1, 0.5, 0.5, 1)
        text_node.set_shadow(0.05, 0.05)
        text_node.set_shadow_color(1, 1, 0, 1)
        text_node.set_frame_color(0, 0, 1, 1)
        text_node.set_frame_as_margin(0.2, 0.2, 0.1, 0.1)
        text_np = self.render.attach_new_node(text_node)
        shader = Shader.make(Shader.SL_GLSL, VERT_SHADER, FRAG_SHADER)
        geom_node_root = NodePath(text_node.get_internal_geom())
        text_geom_node = geom_node_root.find("**/text")
        text_geom_node.set_shader(shader)
        frame_geom_node = geom_node_root.find("**/frame")
        frame_geom_node.set_shader(shader)


app = MyApp()
app.run()

The shader isn’t perfect, as I don’t really know how to render it with the correct colors. Maybe someone else knows how to fix this.

When you run the code sample, you should see that the frame isn’t curved:

This is because the line geom used for the frame only contains 4 vertices. It should be possible to use an additional geometry shader that creates a number of intermediate vertices adaptively, based on the angle between the original vertices on the circle. This is something I might still try when I find some time.

[edit]
Okay, I think I managed to curve the frame as well:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase


VERT_SHADER = """
    #version 130

    uniform mat4 p3d_ModelViewProjectionMatrix;
    uniform mat4 p3d_TextureMatrix;
    uniform float radius;

    in vec4 p3d_Vertex;
    in vec4 p3d_Color;
    in vec2 p3d_MultiTexCoord0;

    out vec2 v_texcoord;
    out vec4 v_color;

    void main() {
        vec4 pos;
        float angle, x, y;
        // the angle of the vertex depends on its x-coordinate as well as the
        // radius of the circle to warp the text along
        angle = atan(p3d_Vertex.x, radius);
        x = sin(angle) * radius;
        y = -cos(angle) * radius;
        pos = vec4(x, y, p3d_Vertex.z, 1.);
        gl_Position = p3d_ModelViewProjectionMatrix * pos;
        v_texcoord = (p3d_TextureMatrix * vec4(p3d_MultiTexCoord0, 0, 1)).xy;
        v_color = p3d_Color;
    }
"""

FRAG_SHADER = """
    #version 130

    uniform sampler2D p3d_Texture0;

    in vec2 v_texcoord;
    in vec4 v_color;

    out vec4 color;

    void main() {
        color = v_color * texture2D(p3d_Texture0, v_texcoord);
    }
"""


VERT_SHADER_FRAME = """
    #version 330

    uniform mat4 p3d_ModelViewProjectionMatrix;
    in vec4 p3d_Vertex;
    in vec4 p3d_Color;

    out Vertex
    {
        vec4 color;
    } vertex;

    void main() {
        gl_Position = p3d_Vertex;
        vertex.color = p3d_Color;
    }
"""

GEOM_SHADER_FRAME = """
    #version 330

    layout(lines) in;

    layout(line_strip, max_vertices=100) out;

    uniform mat4 p3d_ModelViewProjectionMatrix;
    uniform mat4 p3d_ModelViewMatrix;
    uniform float radius;

    in Vertex
    {
        vec4 color;
    } vertex[];

    out vec4 g_color;

    void main()
    {

        vec4 pos;
        float angle1, angle2, angle, delta_angle, x, y;
        float min_angle = .1745;  // minimum angle between points in radians
        int count;

        vec4 P1 = gl_in[0].gl_Position;
        vec4 P2 = gl_in[1].gl_Position;

        g_color = vertex[0].color;

        // the angle of the vertex depends on its x-coordinate as well as the
        // radius of the circle to warp the text along
        angle1 = atan(P1.x, radius);
        angle2 = atan(P2.x, radius);

        // draw first point of original line
        x = sin(angle1) * radius;
        y = -cos(angle1) * radius;
        pos = vec4(x, y, P1.z, 1.);
        gl_Position = p3d_ModelViewProjectionMatrix * pos;
        EmitVertex();

        if (P2.z == P1.z) {
            // determine how many intermediate points to add in order to obtain
            // a smooth curve, based on the minimum angle between any 2 points
            angle = angle2 - angle1;
            count = int(abs(angle) / min_angle);
            delta_angle = angle / count;

            for (int i = 0; i < count; ++i) {
                angle = angle1 + delta_angle * i;
                x = sin(angle) * radius;
                y = -cos(angle) * radius;
                pos = vec4(x, y, P1.z, 1.);
                gl_Position = p3d_ModelViewProjectionMatrix * pos;
                EmitVertex();
            }
        }

        // draw second point of original line
        x = sin(angle2) * radius;
        y = -cos(angle2) * radius;
        pos = vec4(x, y, P2.z, 1.);
        gl_Position = p3d_ModelViewProjectionMatrix * pos;
        EmitVertex();

        EndPrimitive();

    }
"""

FRAG_SHADER_FRAME = """
    #version 330

    uniform vec4 p3d_ColorScale;

    in vec4 g_color;

    out vec4 out_color;

    void main() {
        vec3 base_color;
        out_color = g_color * p3d_ColorScale;
    }
"""


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.disable_mouse()
        self.camera.set_pos(0., -30., 25.)
        self.camera.look_at(0., 0., 0.)
        text_node = TextNode("text")
        text_node.set_text("This is some curved text, warped to fit around a circle.")
        text_node.set_align(TextNode.A_center)
        text_node.set_slant(0.3)
        text_node.set_text_color(1, 0.5, 0.5, 1)
        text_node.set_shadow(0.05, 0.05)
        text_node.set_shadow_color(1, 1, 0, 1)
        text_node.set_frame_color(0, 0, 1, 1)
        text_node.set_frame_as_margin(0.2, 0.2, 0.1, 0.1)
        text_np = self.render.attach_new_node(text_node)
        shader = Shader.make(Shader.SL_GLSL, VERT_SHADER, FRAG_SHADER)
        frame_shader = Shader.make(Shader.SL_GLSL, VERT_SHADER_FRAME, FRAG_SHADER_FRAME, GEOM_SHADER_FRAME)
        radius = 10.
        geom_node_root = NodePath(text_node.get_internal_geom())
        text_geom_node = geom_node_root.find("**/text")
        text_geom_node.set_shader(shader)
        text_geom_node.set_shader_input("radius", radius)
        frame_geom_node = geom_node_root.find("**/frame")
        frame_geom_node.set_shader(frame_shader)
        frame_geom_node.set_shader_input("radius", radius)


app = MyApp()
app.run()

[/edit]

Even if you’re not interested in using a shader, the code does show how to access a TextNode’s internal GeomNode – actually there can be multiple: one for the text itself, another for the frame and a third one for the card.
To get the one with the text, just call find("**/text") on the NodePath that you wrap around the node obtained by the call to TextNode.get_internal_geom.

Anyway, I hope this has been helpful to at least some extent! :slight_smile:

5 Likes

Thanks a lot! Sorry I was too busy to test the solution but it looks great. Certainly better than my approach.

1 Like