Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rendering an image as a node in Three.js with SVGRenderer (or otherwise rendering spheres)

I have a <circle> element in an SVG document, to which I apply a <radialGradient> to give the illusion of it being a sphere:

<svg version="1.1" id="sphere_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="640px" height="640px" viewBox="0 0 640 640" enable-background="new 0 0 640 640" xml:space="preserve">
    <defs>
        <radialGradient id="sphere_gradient" cx="292.3262" cy="287.4077" r="249.2454" fx="147.7949" fy="274.5532" gradientTransform="matrix(1.0729 0 0 1.0729 -23.3359 -23.3359)" gradientUnits="userSpaceOnUse">
            <stop id="sphere_gradient_0" offset="0" style="stop-color:#F37D7F"/>
            <stop id="sphere_gradient_1" offset="0.4847" style="stop-color:#ED1F24"/>
            <stop id="sphere_gradient_2" offset="1" style="stop-color:#7E1416"/>
        </radialGradient>
    </defs>
    <circle fill="url(#sphere_gradient)" cx="320" cy="320" r="320"/>
</svg>

It looks something like this:

red sphere

JSFiddle

I can render this in a three.js WebGLRenderer container by using Gabe Lerner's canvg library:

/* sphere_asset is a div containing the svg element */
var red_svg_html = new String($('#sphere_asset').html()); 
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_svg_texture = new THREE.Texture(red_svg_canvas);
var red_particles = new THREE.Geometry();
var red_particle_material = new THREE.PointCloudMaterial({ 
    map: red_svg_texture, 
    transparent: true, 
    size: 0.15, 
    alphaTest: 0.10 
});
var red_particle_count = 25;
for (var p = 0; p < red_particle_count; p++) {
    var pX = 0.9 * (Math.random() - 0.5),
        pY = 0.9 * (Math.random() - 0.5),
        pZ = 0.9 * (Math.random() - 0.5),
        red_particle = new THREE.Vector3(pX, pY, pZ);
    red_particles.vertices.push(red_particle);
}
var red_particle_system = new THREE.PointCloud(red_particles, red_particle_material);
scene.add(red_particle_system);

So far, so good. I can even programmatically modify the gradient and render different categories of particles:

spheres

What I would like to do is now switch over from WebGLRenderer to using an SVGRenderer, so that I can allow the end user to set the desired orientation and then export a vector image (SVG, or converted to PDF on the back end) that can be used for publication-quality work.

Using the SVG sandbox example from three.js as the basis for experimentation, I have tried a couple different techniques and have not had much luck. I'm hoping someone with experience with three.js may have some suggestions.

My first attempt was to use canvg to render the SVG into a PNG image, and then apply that to an <image> node:

var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_png_data = red_svg_canvas.toDataURL('image/png');
var red_node = document.createElementNS('http://www.w3.org/2000/svg', 'image');
red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', red_png_data);
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'height', '10');
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'width', '10');
var red_particle_count = 25;
for (var i = 0; i < red_particle_count; i++) {
    var object = new THREE.SVGObject(red_node.cloneNode());
    object.position.x = 0.9 * (Math.random() - 0.5);
    object.position.y = 0.9 * (Math.random() - 0.5);
    object.position.z = 0.9 * (Math.random() - 0.5);
    scene.add(object);
}

No nodes show up in my viewbox.

The next thing I tried was a THREE.Sprite object, using canvg and THREE.Texture routines:

var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_svg_texture = new THREE.Texture(red_svg_canvas);
red_svg_texture.needsUpdate = true;
var red_sprite = THREE.ImageUtils.loadTexture(red_png_data);
var red_particle_count = 25;
for (var p = 0; p < red_particle_count; p++) {
    var material = new THREE.SpriteMaterial( { 
        map: red_svg_texture, 
        transparent: true, 
        size: 0.15, 
        alphaTest: 0.10 
    });
    var sprite = new THREE.Sprite( material );
    sprite.position.x = 0.9 * (Math.random() - 0.5),
    sprite.position.y = 0.9 * (Math.random() - 0.5),
    sprite.position.z = 0.9 * (Math.random() - 0.5),
    sprite.scale.set(0.1, 0.1, 0.1);
    scene.add(sprite);
}

This was slightly better, in that I get white, opaque boxes where the spheres would otherwise appear in the rendered viewbox.

A third attempt was made to create an <svg> to nest within the parent SVG node, which contains a reference-able <radialGradient> with the id #sphere_gradient:

var xmlns = "http://www.w3.org/2000/svg";
var svg = document.createElementNS(xmlns, 'svg');
svg.setAttributeNS(null, 'version', '1.1');
svg.setAttributeNS(null, 'x', '0px');
svg.setAttributeNS(null, 'y', '0px');
svg.setAttributeNS(null, 'width', '640px');
svg.setAttributeNS(null, 'height', '640px');
svg.setAttributeNS(null, 'viewBox', '0 0 640 640');
svg.setAttributeNS(null, 'enable-background', 'new 0 0 640 640');

var defs = document.createElementNS(xmlns, "defs");
var radialGradient = document.createElementNS(xmlns, "radialGradient");
radialGradient.setAttributeNS(null, "id", "sphere_gradient");
radialGradient.setAttributeNS(null, "cx", "292.3262");
radialGradient.setAttributeNS(null, "cy", "287.4077");
radialGradient.setAttributeNS(null, "r", "249.2454");
radialGradient.setAttributeNS(null, "fx", "147.7949");
radialGradient.setAttributeNS(null, "fy", "274.5532");
radialGradient.setAttributeNS(null, "gradientTransform", "matrix(1.0729 0 0 1.0729 -23.3359 -23.3359)");
radialGradient.setAttributeNS(null, "gradientUnits", "userSpaceOnUse");

var stop0 = document.createElementNS(null, "stop");
stop0.setAttributeNS(null, "offset", "0");
stop0.setAttributeNS(null, "stop-color", "#f37d7f");
radialGradient.appendChild(stop0);

var stop1 = document.createElementNS(null, "stop");
stop1.setAttributeNS(null, "offset", "0.4847");
stop1.setAttributeNS(null, "stop-color", "#ed1f24");
radialGradient.appendChild(stop1);

var stop2 = document.createElementNS(null, "stop");
stop2.setAttributeNS(null, "offset", "1");
stop2.setAttributeNS(null, "stop-color", "#7e1416");
radialGradient.appendChild(stop2);

defs.appendChild(radialGradient);

svg.appendChild(defs);

var red_circle = document.createElementNS(xmlns, "circle")
red_circle.setAttribute('fill', 'url(#sphere_gradient)');
red_circle.setAttribute('r', '320');
red_circle.setAttribute('cx', '320');
red_circle.setAttribute('cy', '320');
svg.appendChild(red_circle);

var red_particle_count = 25;
for (var i = 0; i < red_particle_count; i++) {
    var object = new THREE.SVGObject(svg.cloneNode(true));
    object.position.x = 0.85 * (Math.random() - 0.5);
    object.position.y = 0.85 * (Math.random() - 0.5);
    object.position.z = 0.85 * (Math.random() - 0.5);
    scene.add(object);
}

No nodes are rendered. Adjustments of the <circle> element's r, cx or cy do not change the end result.

Interestingly, if I change the fill attribute from url(#sphere_gradient) to red, I get a large circle mostly rendered outside my viewbox, which is not attached to the scene (it does not rotate with other elements in my parent scene, like the sides of a cube).

Is there a (working and performant) way to draw spheres or rounded, sphere-like particles in space using a SVGRenderer in three.js?

like image 260
Alex Reynolds Avatar asked Dec 24 '14 21:12

Alex Reynolds


People also ask

What is a renderer in three js?

The Renderer displays the scene onto a HTML Canvas Element. By default it uses WebGL. WebGL allows GPU-accelerated image processing and effects as the renderer creates the 2D image for the Canvas.

Does Three js use OpenGL?

Three. js uses the WebGL engine in the browser for rendering scenes. The API is based on OpenGL (GL stands for graphics library), a desktop graphics API.

What are three js scenes?

Scenes allow you to set up what and where is to be rendered by three. js. This is where you place objects, lights and cameras.


1 Answers

Most attributes in SVG are in the null namespace so

red_node.setAttributeNS('http://www.w3.org/2000/svg', 'height', '10');
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'width', '10');

is correctly written as

red_node.setAttribute('height', '10');
red_node.setAttribute('width', '10');

FWIW

red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', red_png_data);

should ideally be written as

red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', red_png_data);

although in this case your original form will work in most UAs.

In the final example the stop elements must be in the SVG namespace i.e.

var stop0 = document.createElementNS(null, "stop");

should be

var stop0 = document.createElementNS(xmlns, "stop");

A gradient without stops is not drawn at all so that's why you don't see anything till you change the fill to red.

SVG has a painters model. Things are drawn in the order they occur in the file. If you want something to go on top of something else you need to place it later in the file.

like image 126
Robert Longson Avatar answered Oct 15 '22 16:10

Robert Longson