Is there any way to render svg to png

I have not been very active here in a while but I had an idea. Basically instead of having textures I could have an svg file that gets rendered to a texture. Like a texture equivalent to sequenced music. This would massively save file size. Here is an example of my thought process:

This is an extremely simple 257 byte svg:

<svg viewBox="-50 -50 100 100" xmlns="http://www.w3.org/2000/svg"> <circle r="50" style="fill:#000000"/> <circle r="40" style="fill:#008000"/> <circle r="30" style="fill:#00ff00"/> <circle r="20" style="fill:#000000"/> <circle r="10" style="fill:#ffffff"/> </svg>

If I pre render this to a 256x256 png I get a 13.3 kb image

CIRCLE_TEST

That does not seem so bad, but take this 459 byte svg:

<svg viewBox="-50 -50 100 100" xmlns="http://www.w3.org/2000/svg"> <defs> <radialGradient id="grad" cx="50%" cy="50%" r="50%" fx="50%" fy="50%"> <stop offset="0%" stop-color="#ff0000" /> <stop offset="20%" stop-color="#ff8000"/> <stop offset="40%" stop-color="#ffff00"/> <stop offset="60%" stop-color="#00ff00"/> <stop offset="80%" stop-color="#0000ff"/> <stop offset="100%" stop-color="#ff00ff"/> </radialGradient> </defs> <circle r="50" style="fill:url(#grad)"/> </svg>

Rendering it to a 256x256 png results in a 32.9 kb image:

CIRCLE_TEST_2

If I (the player) want a higher resolution, rendering at 1024x1024 it becomes 267 kb:

f I start with a low resolution texture, like the 13.9 kb 256x256 texture, yes it would have a small file size but it would only be as large as 256x256. If the player wants a higher resolution, too bad. But if I start with the 267 kb 1024x1024 texture It could be scaled down if the player selects a lower resolution to conserve ram, but in the files it would still occupy 267 kb forever. Multiply that with however many textures are in the game and that is a rather large occupation.

My working theory is if I create a texture as an svg and somehow rendered it to a texture during runtime I could save several kb per texture, although the rendered texture would occupy the same amount of ram as a premade texture.

With my svg method I can have both. The file size is extremely small, and it can have whatever arbitrary resolution needed for optimal ram usage depending on player system specifications. This would basically give me the ability to change the resolution of the textures infinitely upwards.

Lets explore a hypothetical real world example:

This is a gas mask filter texture from half life, 32x32 1.68 kb:

filter

Very small file size, but if the player wants a higher resolution that is not possible.

Here I remade the image in svg, it is 1,181 bytes (I could optimize it more):

<svg viewBox="-50 -50 100 100" xmlns="http://www.w3.org/2000/svg"> <defs> <g id="HOLE"> <circle r="5" style="fill:#000000" transform="translate(-35 0)"/> <circle r="5" style="fill:#000000" transform="translate(35 0)"/> <circle r="5" style="fill:#000000" transform="translate(0 -35)"/> <circle r="5" style="fill:#000000" transform="translate(0 35)"/> </g> <g id="COICLE"> <circle r="50" style="fill:#000000"/> <circle r="45" style="fill:#304020"/> <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5"/> <polyline points="-49,0 49,0" style="stroke:#000000;stroke-width:5"/> <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5" transform="rotate(45)"/> <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5" transform="rotate(-45)"/> <circle r="12.5" style="fill:#000000"/> <circle r="7.5" style="fill:#ffffff"/> <use href="#HOLE" transform="rotate(22.5)"/> <use href="#HOLE" transform="rotate(-22.5)"/> </g> </defs> <rect width="100" height="100" x="-50" y="-50" fill="#808080" /> <polyline points="0,0 -21.875,0" style="stroke:#304020;stroke-width:100" transform="translate(43.75 0)"/> <use href="#COICLE" transform="translate(-15.625 0) scale(0.5 0.75)"/> </svg>

The image is simpler and “flatter” because this was just a quick test. I could spend a much longer time perfecting it with gradients and noise if I wanted to.

Rendered to 32x32 to match the half life texture I get this, 1.03 kb:

FILTER_TEST

But unlike the half life texture, I can rescale this to a 12.7 kb 256x256 texture:

FILTER_TEST_2

And I could scale it at any resolution I wanted while the file only occupies 1,183 bytes. Multiply this by every texture in a game and this system would massively reduce file size.

So is there any way I could do this in playcanvas?

Sort of. It would need to be rasterised first at runtime before it can used as a texture.

Do you know of any way to do this?

If you turn the svg string into a blob and then a url, you can create an image object and use the url as the source.
From there make a texture object and use it in a material object and add that material to something. I don’t have a computer to test this on, so if this doesn’t work, let me know.

const svgString = `
<svg viewBox="-50 -50 100 100" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <g id="HOLE">
      <circle r="5" style="fill:#000000" transform="translate(-35 0)"/>
      <circle r="5" style="fill:#000000" transform="translate(35 0)"/>
      <circle r="5" style="fill:#000000" transform="translate(0 -35)"/>
      <circle r="5" style="fill:#000000" transform="translate(0 35)"/>
    </g>
    <g id="COICLE">
      <circle r="50" style="fill:#000000"/>
      <circle r="45" style="fill:#304020"/>
      <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5"/>
      <polyline points="-49,0 49,0" style="stroke:#000000;stroke-width:5"/>
      <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5" transform="rotate(45)"/>
      <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5" transform="rotate(-45)"/>
      <circle r="12.5" style="fill:#000000"/>
      <circle r="7.5" style="fill:#ffffff"/>
      <use href="#HOLE" transform="rotate(22.5)"/>
      <use href="#HOLE" transform="rotate(-22.5)"/>
    </g>
  </defs>
  <rect width="100" height="100" x="-50" y="-50" fill="#808080" />
  <polyline points="0,0 -21.875,0" style="stroke:#304020;stroke-width:100" transform="translate(43.75 0)"/>
  <use href="#COICLE" transform="translate(-15.625 0) scale(0.5 0.75)"/>
</svg>
`;

const svgBlob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(svgBlob);

const img = new Image();
img.onload = function () {
    // Create PlayCanvas texture
    const texture = new pc.Texture(app.graphicsDevice, {
        width: img.width,
        height: img.height,
        format: pc.PIXELFORMAT_R8_G8_B8_A8,
        autoMipmap: true
    });
    texture.setSource(img);
    texture.upload();

    // Apply to a material
    const material = new pc.StandardMaterial();
    material.diffuseMap = texture;
    material.update();

    // Assign to a mesh or sprite element
    const entity = app.root.findByName("MyImageEntity");
    entity.model.meshInstances[0].material = material;

    URL.revokeObjectURL(url);
};
img.src = url;

1 Like

Image is not defined, is it just a variable? Also, would it be possible to assign the texture to an extant material?

My high level understanding is that you need to create an off screen canvas, render the SVG to it and export the canvas contents into a Base64 URL to load as a texture asset

Honestly this is all quite new to me so I am not sure what that means.

I’d give all the details to ChatGPT and ask if can write a script to do this. Then go from there.

Hi @ALUCARD,

To simplify the process, you might want to consider a library like html-to-image:

Basically, you could place the svg file (Creating a DOM element) behind your application, and this library will then generate an image texture for you. If you use the new ESM Scripting technique, it should be pretty easy to import the library into your project:

After you convert the SVG with html-to-image, you should be able to feed the result into you image element.I haven’t been able to personally try this particular method iwht Playcanvas, but it was very helpful to me in generating custom PDF’s in a different project I had.

I hope this is helpful.

1 Like

@ALUCARD you should be able to use something like this:

const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <rect width="100" height="100" fill="gold" />
</svg>`;

const blob = new Blob([svgString], { type: 'image/svg+xml' });
await createImageBitmap(blob)

never been called lacard before

Alright I tried to ask an ai to write a script that does this and I genuinely spend maybe 15 minutes fighting with it just for it to do half of what I wanted.

This is the project:

SVG TO TEXTURE | Launch

This is the code:

SVG TO TEXTURE | Code Editor

For reference the object on the right has the script attached to it, the object on the left does not but it shares the same material. This is the code I had to wrestle from the jaws of very artificial intelligence:

// Svgtotexture.js

var Svgtotexture = pc.createScript('svgtotexture');

// Optional: a material asset in your Assets panel to override
Svgtotexture.attributes.add('TheMaterial', {
    type: 'asset',
    assetType: 'material',
});

// Optional debug flags
Svgtotexture.attributes.add('showCanvasDebug', {
    type: 'boolean',
    default: false,
    title: 'Show Canvas Preview'
});
Svgtotexture.attributes.add('useEmissive', {
    type: 'boolean',
    default: false,
    title: 'Fallback to Emissive Map'
});

Svgtotexture.prototype.initialize = function() {
    var app = this.app;

    // --- Your inline SVG string ---
    var svgString = `
<svg viewBox="-50 -50 100 100" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <g id="HOLE">
      <circle r="5" style="fill:#000000" transform="translate(-35 0)"/>
      <circle r="5" style="fill:#000000" transform="translate(35 0)"/>
      <circle r="5" style="fill:#000000" transform="translate(0 -35)"/>
      <circle r="5" style="fill:#000000" transform="translate(0 35)"/>
    </g>
    <g id="COICLE">
      <circle r="50" style="fill:#000000"/>
      <circle r="45" style="fill:#304020"/>
      <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5"/>
      <polyline points="-49,0 49,0" style="stroke:#000000;stroke-width:5"/>
      <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5" transform="rotate(45)"/>
      <polyline points="0,-49 0,49" style="stroke:#000000;stroke-width:5" transform="rotate(-45)"/>
      <circle r="12.5" style="fill:#000000"/>
      <circle r="7.5" style="fill:#ffffff"/>
      <use href="#HOLE" transform="rotate(22.5)"/>
      <use href="#HOLE" transform="rotate(-22.5)"/>
    </g>
  </defs>
  <rect width="100" height="100" x="-50" y="-50" fill="#808080" />
  <polyline points="0,0 -21.875,0" style="stroke:#304020;stroke-width:100" transform="translate(43.75 0)"/>
  <use href="#COICLE" transform="translate(-15.625 0) scale(0.5 0.75)"/>
</svg>
    `;

    // 1) Rasterize into offscreen canvas
    svgToCanvas(svgString).then(function(canvas) {


        // 2) Build & upload the texture
        var tex = new pc.Texture(app.graphicsDevice, {
            width:      canvas.width,
            height:     canvas.height,
            format:     pc.PIXELFORMAT_R8_G8_B8_A8,
            autoMipmap: true
        });
        tex.setSource(canvas);
        tex.upload();
        console.log('SVG→Texture ready (WxH):', tex.width, tex.height);

        // 3) Apply to this.entity’s material
        var mi = this.entity.render && this.entity.render.meshInstances[0];
        if (mi) {
            var mat = mi.material.clone();
            // older engine versions use diffuseMap
            mat.diffuseMap = tex;
            // newer engine versions use albedoMap
            mat.albedoMap  = tex;
            // optional emissive fallback
            if (this.useEmissive) {
                mat.emissiveMap = tex;
            }
            mat.update();
            mi.material = mat;
        } else {
            console.warn('Svgtotexture: no renderComponent found on this.entity.');
        }

        // 4) (Optional) Override a shared material asset
        if (this.TheMaterial) {
            var matAsset = app.assets.get(this.TheMaterial);
            if (matAsset) {
                matAsset.ready(function() {
                    var sharedMat = matAsset.resource;
                    sharedMat.diffuseMap  = tex;
                    sharedMat.albedoMap   = tex;
                    if (this.useEmissive) {
                        sharedMat.emissiveMap = tex;
                    }
                    sharedMat.update();
                }.bind(this));
            } else {
                console.warn('Svgtotexture: TheMaterial asset not found.');
            }
        }

    }.bind(this)).catch(function(err) {
        console.error('SVG→Canvas raster failed:', err);
    });
};

// Helper: convert SVG string → HTMLCanvasElement
function svgToCanvas(svg) {
    return new Promise(function(resolve, reject) {
        var blob = new Blob([svg], { type: 'image/svg+xml' });
        var url  = URL.createObjectURL(blob);
        var img  = new Image();
        img.crossOrigin = 'Anonymous';
        img.onload = function() {
            var c = document.createElement('canvas');
            c.width  = img.naturalWidth;
            c.height = img.naturalHeight;
            c.getContext('2d').drawImage(img, 0, 0);
            URL.revokeObjectURL(url);
            resolve(c);
        };
        img.onerror = reject;
        img.src = url;
    });
}

Currently the code does convert the svg into an image and apply it to the entity with the script. What I want the code to do is give the texture to the material instead of the entity itself.

It’s pretty close.

You can remove section 3 to stop it cloning the material on the entity and applying the texture to the clone material.

Section 4 has an error where it’s trying to get the material asset from the registry when the script already has the material asset from the attribute

Change

            var matAsset = app.assets.get(this.TheMaterial);

to

            var matAsset = this.TheMaterial;

Example project PlayCanvas | HTML5 Game Engine

1 Like

Yes that does work. For some reason when I try to change the width with an attribute it changes to a string or something. Just look at the project and you will see what I am talking about:

SVG TO TEXTURE | Editor

NVM I fixed it, it was a “this” mislocation error

Now there are only 2 things needed for this script to be perfect, the ability to assign the texture to an array of multiple materials, and the ability to use a text file attribute instead of hard coded svg code.

1 Like

I have accomplished both.

2 Likes

I just got a notification from you changing the name, neat.