[SOLVED] Bend a simple line texture into a circle?

Hello Cocos users! :blush:

Iā€™m making a game in which I have to draw many different circles using a simple line texture.
In fact from that :
I want to get that :
(I apologize for those ugly MS Paint examples :mask:)

Unfortunately I canā€™t be using pre-rendered .png as there can be more than 200 different radius (and I really need every of them).

The second solution was to draw every of them into a new CCTexture2D (Iā€™m okay with the maths underlaying) and use it for creating a CCSprite but I donā€™t know about the memory it will useā€¦

Do you have any idea of how I can achieve this effect? :kissing_smiling_eyes:

Iā€™m not new to game devā€™ but I just started using Cocos2d-x so I may be slow to understand but thank you in advance for trying (and please forgive me for my frenchy english)! :relaxed:

You can you just one circle really large, and scale it for every dimension you want.

Hi milos1290 :blush:

Thank you for your reply but I regret I canā€™t do this for two reasons :
ā€¢ The stroke width would not be equal from a circle to an other. If I design the biggest one (letā€™s say with radius = 200 and stroke width = 10), then the one with radius = 100 will have a stroke width = 5.
ā€¢ I showed you an example in which the pattern doesnā€™t change along the x axis but imagine a texture with blue dots every 10 pixels. I want those same dots spaced out with (about) 10 pixels on every circle, regardless of their radius (this involves that the smallest the circleā€™s radius is, the fewest dots there is on it).

Iā€™m not very skilled in OpenGL but what Iā€™m looking for is to give it my line texture and an array of vertices (pixel pos + texture coords) that describes the shape I want.

The only contraint is that it has to be powerful as there can be up to 1000 of those circles with about 20 appearing on the screen.

I get it. There is another solution. You can use DrawNode and draw circles the way you want. Or you can directly draw with OpenGL and custom commands. Though you will need knowledge of OpenGL, shaders and basic trigonometry.

Try using motionstreak

Iā€™d recommend building your own mesh.

something like:

  • define segment count (number of verts on a given radius)
  • create outer verts (outside radius)
  • create innter verts (inside radius)
  • map UVs: - x = normalisedAngle * outerRadius, y = isInner ? 0 : 1
  • generate indices for wrapped triangle list connecting inner and outer verts

This will draw a circle with line thickness (outerRadius - innerRadius). If you have a texture that requires correct wrapping you will need to ensure your circleā€™s outerRadius is a whole number.

Hope this helps.

Hi duke4e and almax27 :blush:

Thank you for your replies, this really helps!

I think that what Iā€™m looking for is exactly what you said @almax27, build my own mesh using OpenGL triangles.
Please can you just give me the classes I have to use to achieve this effect ? I looked into the API doc but itā€™s a little bit messy and I didnā€™t figure out how. :confused:

Thank your for your help, once again this really helps :relaxed:

Yep, best way is a custom mesh. I would go in that direction.

aye openGL is quite tricky, if youā€™ve not had much experience with it then Iā€™d recommend spending a few days researching the API and implementing some bare bones examples away from cocos2d-x.
http://www.opengl-tutorial.org/

However this did sound like a fun little project so Iā€™ve written the class for you :smiley:

download
TexturedRing.zip (9.2 KB)

image
this must be a power-of-two texture to support GL_REPEAT

result

TexturedRingTest.h - useage

//almaxmasuma@gmail.com - 2015/08/11
#include "cocos2d.h"
#include "TexturedRing.h"

cocos2d::Scene* createTexturedRingScene()
{
    auto director = cocos2d::Director::getInstance();
    auto center = director->getVisibleOrigin() + director->getVisibleSize() * 0.5f;
    auto scene = cocos2d::Scene::create();
    {
        auto ring = TexturedRing::create(30, 32, "ring_test.png");
        ring->setPosition(center);
        scene->addChild(ring);
    }
    {
        auto ring = TexturedRing::create(60, 32, "ring_test.png");
        ring->setStrokeScale(0.2f);
        ring->setPosition(center);
        scene->addChild(ring);
    }
    {
        auto ring = TexturedRing::create(100, 32, "ring_test.png");
        ring->setPosition(center);
        scene->addChild(ring);
    }
    {
        auto ring = TexturedRing::create(160, 32, "ring_test.png");
        ring->setStrokeScale(0.5f);
        ring->setIsTextureRepeated(false);
        ring->setPosition(center);
        scene->addChild(ring);
    }
    return scene;
}

TexturedRing.h

 //almaxmasuma@gmail.com - 2015/08/11
#pragma once

#include "cocos2d.h"

class TexturedRing : public cocos2d::Node, public cocos2d::TextureProtocol
{
public:
    virtual ~TexturedRing();
    
    //static constructors
    static TexturedRing* create(float radius, unsigned int segmentCount, const std::string& imagePath);
    static TexturedRing* create(float innerRadius, unsigned int segmentCount, cocos2d::Texture2D* texture);
    
    //--Node--
    void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4& transform, uint32_t flags) override;
    
    //--TextureProtocol--
    virtual cocos2d::Texture2D* getTexture() const override;
    virtual void setTexture(cocos2d::Texture2D *texture) override;
    
    //--BlendProtocol--
    virtual void setBlendFunc(const cocos2d::BlendFunc &blendFunc) override;
    virtual const cocos2d::BlendFunc& getBlendFunc() const override;
    
    //--helper methods--
    bool buildGeometry(float radius, unsigned int segmentCount);
    
    //--configuration--
    //scales the thickness of line and repeat rate
    CC_PROPERTY_PASS_BY_REF(float, m_strokeScale, StrokeScale);
    //true by default. The texture is repeated around the ring. Textures must be PoT for GL_REPEAT
    CC_PROPERTY_PASS_BY_REF(bool, m_isTextureRepeated, IsTextureRepeated);
    
CC_CONSTRUCTOR_ACCESS:
    TexturedRing();
    bool init(float radius, unsigned int segmentCount, const std::string& imagePath);
    bool initWithTexture(float radius, unsigned int segmentCount, cocos2d::Texture2D* texture);
    
    //texture
    cocos2d::Texture2D* m_texture;
    cocos2d::BlendFunc m_blendFunc;
    
    //geometry
    float m_radius;
    unsigned int m_segmentCount;
    std::vector<cocos2d::Vec2> m_vertices;
    std::vector<cocos2d::Tex2F> m_texCoords;
    
    //rendering
    void onDraw(const cocos2d::Mat4 &transform, uint32_t flags);
    cocos2d::CustomCommand _customCommand;
    
private:
    CC_DISALLOW_COPY_AND_ASSIGN(TexturedRing);
};

TexturedRing.cpp

//almaxmasuma@gmail.com - 2015/08/11
#include "TexturedRing.h"

USING_NS_CC;

TexturedRing::TexturedRing()
: m_texture(nullptr)
, m_radius(0)
, m_segmentCount(0)
, m_strokeScale(1)
, m_isTextureRepeated(true)
{
    
}

TexturedRing::~TexturedRing()
{
    CC_SAFE_RELEASE_NULL(m_texture);
}

TexturedRing* TexturedRing::create(float radius, unsigned int segmentCount, const std::string& imagePath)
{
    auto instance = new (std::nothrow) TexturedRing();
    if(instance && instance->init(radius, segmentCount, imagePath))
    {
        return instance;
    }
    CC_SAFE_DELETE(instance);
    return nullptr;
}

TexturedRing* TexturedRing::create(float radius, unsigned int segmentCount, Texture2D* texture)
{
    auto instance = new (std::nothrow) TexturedRing();
    if(instance && instance->initWithTexture(radius, segmentCount, texture))
    {
        return instance;
    }
    CC_SAFE_DELETE(instance);
    return nullptr;
}

bool TexturedRing::init(float radius, unsigned int segmentCount, const std::string& imagePath)
{
    auto textureCache = Director::getInstance()->getTextureCache();
    auto texture = textureCache->addImage(imagePath);
    return initWithTexture(radius, segmentCount, texture);
}

bool TexturedRing::initWithTexture(float radius, unsigned int segmentCount, Texture2D* texture)
{
    if(texture == nullptr)
    {
        CCLOGERROR("[TexturedRing] Invalid texture");
        return false;
    }
    
    setAnchorPoint(Vec2(0.5f,0.5f));
    setTexture(texture);
    if(buildGeometry(radius, segmentCount) == false)
    {
        return false;
    }
    
    // configure texture rendering
    if (! texture || ! texture->hasPremultipliedAlpha())
    {
        m_blendFunc = BlendFunc::ALPHA_NON_PREMULTIPLIED;
        setOpacityModifyRGB(false);
    }
    else
    {
        m_blendFunc = BlendFunc::ALPHA_PREMULTIPLIED;
        setOpacityModifyRGB(true);
    }
    
    //set default shader
    setGLProgramState(GLProgramState::getOrCreateWithGLProgramName(GLProgram::SHADER_NAME_POSITION_TEXTURE));
    
    return true;
}

bool TexturedRing::buildGeometry(float radius, unsigned int segmentCount)
{
    if(radius <= 0)
    {
        CCLOGERROR("[TexturedRing] Invalid radius: (%f)", radius);
        return false;
    }
    
    if(segmentCount < 4)
    {
        CCLOGERROR("[TexturedRing] Invalid segment count: (%d), must be 4 or more", segmentCount);
        return false;
    }
    
    auto textureSize = m_texture->getContentSize() * m_strokeScale;
    
    //calculate radii
    auto strokeHalfWidth = textureSize.height * 0.5f;
    auto innerRadius = radius - strokeHalfWidth;
    auto outerRadius = radius + strokeHalfWidth;
    
    //calulate best fitting texture repeat count
    auto circumference = 2.0f*M_PI*radius;
    auto textureRepeatCount = 1;
    if(m_isTextureRepeated)
    {
        textureRepeatCount = MAX(1.0f, roundf((circumference / textureSize.width)));
    }
    
    //build buffers interleaved between inner and outer circles
    size_t vertCount = (segmentCount+1) * 2;
    m_vertices.resize(vertCount);
    m_texCoords.resize(vertCount);
    for(size_t i = 0; i < segmentCount + 1; i++)
    {
        size_t idx = i * 2;
        float tVal = (float)i / segmentCount;
        float angleRadians = 2.0f*M_PI*tVal;
        Vec2 dir = Vec2::forAngle(angleRadians);
        float u = tVal * textureRepeatCount;
        
        //inner
        m_texCoords[idx].u = u;
        m_texCoords[idx].v = 0;
        m_vertices[idx].x = outerRadius + dir.x * innerRadius;
        m_vertices[idx].y = outerRadius + dir.y * innerRadius;
        
        //outer
        m_texCoords[idx+1].u = u;
        m_texCoords[idx+1].v = 1;
        m_vertices[idx+1].x = outerRadius + dir.x * outerRadius;
        m_vertices[idx+1].y = outerRadius + dir.y * outerRadius;
    }
    
    setContentSize(Size(outerRadius*2,outerRadius*2));
    
    //cache configuration
    m_radius = radius;
    m_segmentCount = segmentCount;
    
    return true;
}

Texture2D* TexturedRing::getTexture() const
{
    return m_texture;
}

void TexturedRing::setTexture(Texture2D *texture)
{
    CC_SAFE_RELEASE_NULL(m_texture);
    if(texture != nullptr)
    {
        if(m_isTextureRepeated)
        {
            Texture2D::TexParams textureParams;
            textureParams.minFilter = GL_LINEAR;
            textureParams.magFilter = GL_LINEAR;
            textureParams.wrapS = GL_REPEAT; //default to horizontal wrapping
            textureParams.wrapT = GL_CLAMP_TO_EDGE;
            texture->setTexParameters(textureParams);
        }
        m_texture = texture;
        m_texture->retain();
        //rebuild the geometry to match new texture
        if(m_radius > 0 && m_segmentCount > 0)
        {
            buildGeometry(m_radius, m_segmentCount);
        }
    }
}

void TexturedRing::setBlendFunc(const BlendFunc &blendFunc)
{
    m_blendFunc = blendFunc;
}

const BlendFunc& TexturedRing::getBlendFunc() const
{
    return m_blendFunc;
}

void TexturedRing::setStrokeScale(const float &scale)
{
    if(scale != m_strokeScale)
    {
        m_strokeScale = scale;
        buildGeometry(m_radius, m_segmentCount);
    }
}

const float& TexturedRing::getStrokeScale() const
{
    return m_isTextureRepeated;
}

void TexturedRing::setIsTextureRepeated(const bool &isTextureRepeated)
{
    if(isTextureRepeated != m_isTextureRepeated)
    {
        m_isTextureRepeated = isTextureRepeated;
        setTexture(m_texture);
    }
}

const bool& TexturedRing::getIsTextureRepeated() const
{
    return m_isTextureRepeated;
}

void TexturedRing::onDraw(const Mat4 &transform, uint32_t flags)
{
    if(m_texture == nullptr || m_vertices.empty())
    {
        return;
    }
    
    getGLProgram()->use();
    getGLProgram()->setUniformsForBuiltins(transform);
    
    GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POSITION | GL::VERTEX_ATTRIB_FLAG_TEX_COORD);
    GL::blendFunc( m_blendFunc.src, m_blendFunc.dst );
    
    GL::bindTexture2D( m_texture->getName() );
    
#ifdef EMSCRIPTEN
    setGLBufferData(m_vertices, (sizeof(Vec2) * m_vertices.size()), 0);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, 0);
    
    setGLBufferData(_texCoords, (sizeof(Tex2F) * m_texCoords.size()), 1);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, 0);
#else
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 2, GL_FLOAT, GL_FALSE, 0, m_vertices.data());
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, m_texCoords.data());
#endif // EMSCRIPTEN
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)m_vertices.size());
    CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1, m_vertices.size());
}

void TexturedRing::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    _customCommand.init(_globalZOrder, transform, flags);
    _customCommand.func = CC_CALLBACK_0(TexturedRing::onDraw, this, transform, flags);
    renderer->addCommand(&_customCommand);
}
5 Likes

@almax27 youā€™re amazing! :blush:

Thanks a lot for what youā€™ve done as this will let me focus on the rest of my project without worrying about that :smile:

Iā€™m also glad to have some solid base to begin to work on in order to discover and understand custom OpenGL commands !

Thank you once again, I hope Iā€™ll make good use of your work.
Thanks to the two others, it feels good knowing there are people ready to help you :relaxed:

I set the topic as SOLVED :wink:

1 Like

No problem! Glad to help, and writing it was fun :smiley:

Good luck with your work, it would be cool see your progress too, the community here is always happy to give feedback!

Itā€™s cool effect. Thanks for your work.

But I would also want to know if it is possible to reserve/stretch original image instead of GL_REPEAT

Would be a simple modification, force textureRepeatCount to 1 and remove the setTextParameters call :smiley:

Edit: updated original post with this functionality as a configuration

Iā€™ve just integrated it in my game and everythingā€™s OK :wink:
Iā€™ll post some screenshots as soon as I have something playable :smile:

I just noticed two things I had to correct in your implementation (and that could be good to fix in the uploaded version) :
ā€¢ In method ā€œsetStrokeScaleā€, youā€™re returning ā€œm_isTextureRepeatedā€ instead of ā€œm_strokeScaleā€
ā€¢ You didnā€™t made accessors for the radius so Iā€™ve written them by myself, not forgetting to call ā€œbuildGeometryā€ in method ā€œsetRadiusā€.

Have a good day! :blush:

Thanks for your help. I havenā€™t tried it but the result looks cool.
Later I think I could try evolving it into generalized method.

TextureAlongPath
|ā€“TextureAlongRectangle -> say, useful for UI border decorations?
|ā€“TextureAlongCircle
|ā€“TextureAlongEllipse
|ā€“TextAlongPath
|ā€“ā€¦