Rendering an image as a node in Three.js with SVGR

2019-06-15 07:46发布

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?

3条回答
女痞
2楼-- · 2019-06-15 08:03

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.

查看更多
We Are One
3楼-- · 2019-06-15 08:10

Here is what you need to add to your SVGRenerer.js to fix your problem with rendering Particles:

function renderParticle( v1, element, material, scene ) {


        _svgNode = getCircleNode( _circleCount++ );
        _svgNode.setAttribute( 'cx', v1.x );
        _svgNode.setAttribute( 'cy', v1.y );
        _svgNode.setAttribute( 'r', element.scale.x * _svgWidthHalf );

        if ( material instanceof THREE.ParticleCircleMaterial ) {

            if ( _enableLighting ) {

                _color.r = _ambientLight.r + _directionalLights.r + _pointLights.r;
                _color.g = _ambientLight.g + _directionalLights.g + _pointLights.g;
                _color.b = _ambientLight.b + _directionalLights.b + _pointLights.b;

                _color.r = material.color.r * _color.r;
                _color.g = material.color.g * _color.g;
                _color.b = material.color.b * _color.b;

                _color.updateStyleString();

            } else {

                _color = material.color;

            }

            _svgNode.setAttribute( 'style', 'fill: ' + _color.__styleString );

        }

        _svg.appendChild( _svgNode );


    }
查看更多
Viruses.
4楼-- · 2019-06-15 08:26

In your first attempt, if you add the snap svg library, you could get the svg element(s) from canvg and wrap it into snap structure (as a paper for snap) and then use the paper.toString() method to produce the svg code for your svg File.

var paper = Snap("#... ");        //this wraps the svg element from the canvg onto snap structure 
console.log( paper.toString());   // this gives you the svg code!

Edit: Or you could use fabric.js to convert canvas to svg:

var svg = canvas.toSVG();

Or you could use canvas2svg library.

查看更多
登录 后发表回答