SVG表现3D图形

 
基本上Svg是用来表示2D图形的,但也不是不可以表示3D图形。
 
首先是3D图形位置的处理,我们需要为3D图形建立3D坐标系,并实现3D坐标变换,包括平移,旋转等,具体的实现方法有欧拉法,旋转矩阵法和四元数法等。根据3D模型的顶点坐标,可以实现3D图形在位置上的的显示。由于ADOBE SVG VIEWER提供了丰富的坐标变换函数,包括平移变换(translate), 旋转变换(rotate), 伸缩变换(scale), 歪斜变换(skew), 矩阵变换(matrix)等,因此实现坐标变换相对简单。
 
第二步需要将3D 物体显示在平面上,即3D坐标到2D的变换,因为最终图形还是要在平面上表示出来,这需要考虑以下问题:
 
  •         透视原理,距离越远图形越小,最终图形收缩到一个或多个点(消失点),具体有一点透视(例如例一),两点透视和三点透视。
  •        显示顺序,根据SVG按顺序绘制的原则,图形的距离越近越后画,距离越远越先画,而且被靠近用户的图形覆盖(对复杂图形计算比较麻烦)
  •         图形在不同位置上,根据自身特点(材质,形状)以及光源和摄像机的位置,具有不同的颜色,光照,纹理,清晰度等。这是最麻烦也是最影响速度的一点。
 
下面是两个例子,第一个非常简单,实现了在一个消失点飞出的球体,根据球体的位置进行了坐标变换,以及排序,以保证正确的绘制顺序。
xml version = " 1.0 "  encoding = " utf-8 " ?>
< svg id = " doc "  viewbox = " 0 0 200 200 "  zoomAndPan = " disable "  onload = " init() "  onresize = " sizeChange() " >
    
< desc > SVG test desc >
    
< script type = " text/javascript " > [CDATA[
    
    
var  svgns  =   " http://www.w3.org/2000/svg " ;
    
var  xlinkNS  =   " http://www.w3.org/1999/xlink " ;
    
var  myNS  =   " http://www.svgfun.com/2006/others " ;
    
    
// Transform string    
     var  t1;
    
var  t2;
    
    
// Draw range and center point
     var  maxX,maxY,centerX,centerY;
    
var  maxZ  =   2000 ;
    
    
// Speed and accelerate
     var  maxSpeed  =   100 ;
    
var  maxAccelerate  =   5 ;
    
    
// Refresh interval
     var  interval  =   100 ;
    
    
// Object number
     var  count  =   30 ;
    
var  groupId = " g1 " ;
    
    
var  objList  =   new  Array();
    
    
// /
     // /  Bubble class
     // /
     function  Bubble(id, cx, cy, cz, r, speed, accelerate, fill, transform)
    
{
        
this.id = id;
        
this.cx   = cx;
        
this.cy = cy;
        
this.cz    = cz;
        
this.r = r;
        
this.speed    = speed;
        
this.accelerate = accelerate;
        
this.fill = fill;
        
this.transform = transform;
        
        
this.draw = draw;
        
this.move = move;
    }

    
    
function  draw( groupId )
    
{
        
var group = svgDocument.getElementById( groupId );
        
        
if(group == null)
        
{
            group 
= svgDocument.createElementNS(svgns, "g" )
            group.setAttributeNS(
null,"id",groupId);
            svgDocument.documentElement.appendChild(group);
        }

        
        
var elem = svgDocument.createElementNS(svgns, "circle" );
        elem.setAttributeNS(
null"id"this.id);
        elem.setAttributeNS(
null"cx"this.cx);
        elem.setAttributeNS(
null"cy"this.cy);
        elem.setAttributeNS(
null"r"this.r);
        elem.setAttributeNS(myNS, 
"z"this.cz);                   
        elem.setAttributeNS(myNS, 
"speed"this.speed);           
        elem.setAttributeNS(myNS, 
"accelerate"this.accelerate); 
        elem.setAttributeNS(
null,"fill",this.fill);
        elem.setAttributeNS(
null,"transform",this.transform);
        group.appendChild(elem);
        
    }

    
    
function  move()
    
{
            
var newSpeed;
                  
            
//Set new speed by accelerate
            if( (this.speed + this.accelerate) < maxSpeed )
                newSpeed 
= this.speed +  this.accelerate;
            
else
                newSpeed 
= maxSpeed;
                
            
//Set z distance
            ifthis.cz > this.speed )
            
{
            
this.cz = this.cz - this.speed;
            
this.speed = newSpeed;
        }

            
else
            
{
                
//Set random position and object back to vinish point
                this.cx = Math.random() * maxX;
            
this.cy = Math.random() * maxY;
            
this.cz = maxZ - 1
            
this.speed = Math.random() * maxSpeed;
            
this.accelerate = Math.random() * (maxAccelerate);
        }

    
        
//Set transform
        var scale = (maxZ-this.cz)/maxZ;
        
var t3=" scale("+scale+"";
        
this.transform = t1+t3+t2;
    }
 
    
    
// /
     // /  Bubble class finish
     // /
    
    
    
function  setRange()
    
{
        maxX 
= window.innerWidth;
        maxY 
= window.innerHeight;
        centerX 
= maxX/2;
        centerY 
= maxY/2;
        
        t1 
= "translate("+centerX+","+centerY+")";
        t2 
= "translate(-"+centerX+",-"+centerY+")";
        
        
//alert(t1+t2);
    }

    
    
function  sizeChange()
    
{
        setRange();
        refresh();
    }

    
    
function  refresh()
    
{
        
for (i in objList)
        
{
            objList[i].move();
        }

        
        objList.sort(sortByZ);
        
        
var group = document.getElementById( groupId );
        document.documentElement.removeChild(group);
        
        
for (i in objList)
        
{
            objList[i].draw(groupId);
        }

        
        
var group = document.getElementById( groupId );
        
//alert(printNode(group));
    }

    
    
function  init()
    
{
        setRange();
        
        
for(var i=0; i < count; i++)
        
{
            
var id = "bubble"+i;
            
var cx = Math.random() * maxX;
            
var cy = Math.random() * maxY;
            
var cz = maxZ - 1
            
var r = 50;
            
var speed = Math.random() * maxSpeed;
            
var accelerate = Math.random() * (maxAccelerate);
            
var fill = "url(#radial2)";
            
var transform = "scale(1)";
            
            
var bubble = new Bubble(id, cx, cy, cz, r, speed, accelerate, fill, transform);
            objList[objList.length] 
= bubble;
        }

        
        
//Set position and speed etc
        refresh();
        
        setInterval(
"refresh()",interval);
        
//alert(printNode(instance));
    }


    
function  sortByZ(a,b)
    
{
        
return b.cz - a.cz;
    }
 

    ]]
> script >
    
    
< defs >
    
        
< circle id = " obj1 "  r = ' 50 '  fill = ' url(#radial1) ' >
        
circle >
        
        
< radialGradient id = " radial1 "  cx = " 75% "  cy = " 75% "  r = " 75% " >
            
< stop offset = " 15% "  stop - color = " white "   />
            
< stop offset = " 100% "  stop - color = " red "   />
        
radialGradient >
        
        
< radialGradient id = " radial2 "  cx = " 25% "  cy = " 25% "  r = " 75% " >
            
< stop offset = " 15% "  stop - color = " white "   />
            
< stop offset = " 100% "  stop - color = " blue "   />
        
radialGradient >
        
        
< radialGradient id = " radial3 " >
            
< stop offset = " 15% "  stop - color = " white "   />
            
< stop offset = " 100% "  stop - color = " green "   />
        
radialGradient >
        
    
defs >
    
    
< rect id = " back "  x = " 0 "  y = " 0 "  width = " 100% "  height = " 100% "  fill = " black "  fill - opacity = " 1 " />
    
    
< g id = " g1 " >
    
g >
svg >
 
第二个是根据ADOBE DEMO中的化学元素例子改进,将元素文字改为球体,可以通过鼠标拖动旋转。
< svg width = " 100% "  height = " 100% "  viewBox = " 0 0 350 300 "  xml:space = " preserve "  onload = " Initialize(evt) "  onmousedown = " MouseDown(evt) "  onmousemove = " MouseMove(evt) "  onmouseup = " MouseUp(evt) " >

< title > 3D.svg title >
< desc > An interactive 3d model. desc >

< script language = " Javascript " > [CDATA[
  
            
var  FontScalar            =   8   // 10
             // var ChargeScale        = 0.8
             var  Dimensions            =   200
            
var  CameraDistance        =  Dimensions  *   2
            
var  HitherPlane           =  CameraDistance  /   20
            
var  MouseScalar           =  . 005
            
var  MinMouseSpeed         =  . 01
            
var  MouseMoveAverage      =  . 3
            
var  MaxRotationalSpeed    =  . 05
            
var  RotationsPerCycle     =   4
            
var  ApproachRange         =   3   *  RotationsPerCycle
            
var  BondSpread            =   3
            
var  MinTurnIncrement      =  . 0001
            
var  ShadowDistance        =  Dimensions  /   4
            
var  radio  =   3 ;
            
            
var  SVGDocument  =   null
            
            
var  Rotating  =   true
            
            
var  Scale  =   1
            
            
var  XOffset  =   0
            
var  YOffset  =   0
            
var  ZOffset  =   0

            
var  MinX  =   0
            
var  MaxX  =   0
            
var  MinY  =   0
            
var  MaxY  =   0   
            
var  MinZ  =   0
            
var  MaxZ  =   0   

            
var  Theta  =   0 ;
            
var  Phi    =   0 ;
            
            
var  ThrownTheta  =   0 ;
            
var  ThrownPhi    =   0 ;
          
            
var  Atoms  =   new  Array();
            
var  Bonds  =   new  Array();
            
            
var  debugFlag  =   true // My debug flag;
            
            
function  Initialize(LoadEvent)
              
{
                SVGDocument 
= LoadEvent.getTarget().getOwnerDocument();
    
                GenerateStructure();
     
             
/*
        if(debugFlag == true)
        { 
                    s=printNode(SVGDocument.getElementById("atoms"));
                    alert(s);
                    debugFlag = false;
                }
                
*/

              }

              
              
              
            
function  Atom(x, y, z, fillStyle)
            
{
                FixRange(x, y, z)

                
if (x       != nullthis.x       = x;
                
if (y       != nullthis.y       = y;
                
if (z       != nullthis.z       = z;
                 
                
this.SVG = SVGDocument.createElement("circle");
                
this.SVG.setAttributeNS(null,"fill", fillStyle);
                
//this.SVG.setAttributeNS(null,"filter","url(#dropShadow)");
                SVGDocument.getElementById("atoms").appendChild(this.SVG);
                
                Atoms[Atoms.length] 
= this//Save to array
                this.refresh();
              
                
return this;
            }

              
            Atom.prototype.x            
=   0 ;
            Atom.prototype.y            
=   0 ;
            Atom.prototype.z            
=   0 ;
       
       
            Atom.prototype.bonds        
=   new  Array();
            Atom.prototype.element      
=   "" ;
           
           
            Atom.prototype.displayPhi   
=   0 ;
            Atom.prototype.displayTheta 
=   0 ;
            Atom.prototype.distanceScalar 
=   1 ;
            
            Atom.prototype.refresh 
=  Atom_Refresh;
            Atom.prototype.fixCoords 
=  Atom_FixCoordinates;
            Atom.prototype.calculateRotation 
=  Atom_CalculateRotation
                        
 
            
function  Atom_FixCoordinates()
              
{
                
this.x += XOffset;
                
this.y += YOffset;
                
this.z += ZOffset;
                
                
this.x = this.x * Scale;
                
this.y = this.y * Scale;
                
this.z = this.z * Scale;
                
                
this.refresh()
              }

            
            
function  Atom_Refresh()
              
{
                  oldx 
= this.x
                  oldy 
= this.y
                  
                
this.calculateRotation();
                
                
if (this.SVG != null)
                  
{
                    
this.SVG.setAttribute("transform""translate(" + this.displayX + "" + this.displayY + ")");
                    
this.SVG.setAttributeNS(null,"r"this.displayZ);
                  }

                  
                
//Sort by displayZ  
                var elem = SVGDocument.getElementById("atoms");
              
var nodes = elem.getChildNodes;
              
var insertFlag = false;
              
        
//for(i = 1;i < ((nodes.length)-1);i += 2)
        for(i = 0;i < ((nodes.length-1));i ++)
        
{
            
//Compare by r attribute in SVG node
                    var nodeMap = nodes.item(i).attributes;
                    
var r = nodeMap.getNamedItemNS(null"r");
                                      
                    
if(this.displayZ < r.nodeValue)
                    
{
                        elem.insertBefore(
this.SVG, nodes.item(i));
                        insertFlag 
= true;
                        
break;
                    }
   
                }
       
                        
                
//The nearest one
                if(insertFlag == false)
                    elem.appendChild(
this.SVG);

                
return ((oldx != this.x) || (oldy != this.y))
              }

              
            
function  Atom_CalculateRotation()
              
{
                  
var clipTheta = true;
                  
var clipPhi   = true;
                  
                
for (var I = 1; I < RotationsPerCycle; I++)
                  
{
                    dTheta 
= Theta - this.displayTheta;
                    dPhi   
= Phi   - this.displayPhi;
                
                
                    
if ((dTheta > MaxRotationalSpeed * ApproachRange) || (dTheta < MaxRotationalSpeed * ApproachRange * -1))
                      dTheta 
= max(min(dTheta, MaxRotationalSpeed), MaxRotationalSpeed * -1)
                    
else if ((dTheta < MinTurnIncrement) && (dTheta > MinTurnIncrement * -1))
                      dTheta 
= 0
                    
else
                      
{
                        dTheta 
= dTheta / ApproachRange
                        clipTheta 
= false;
                      }

                    
this.displayTheta += dTheta
                    
                    
if ((dPhi > MaxRotationalSpeed * ApproachRange) || (dPhi < MaxRotationalSpeed * ApproachRange * -1))
                      dPhi 
= max(min(dPhi, MaxRotationalSpeed), MaxRotationalSpeed * -1)
                    
else if ((dPhi < MinTurnIncrement) && (dPhi > MinTurnIncrement * -1))
                      dPhi 
= 0
                    
else
                      
{
                        dPhi 
= dPhi / ApproachRange
                        clipPhi 
= false;
                      }

                    
this.displayPhi += dPhi

                
                    
this.x += dTheta * this.z;
                    
this.y += dPhi   * this.z;
                    
this.z -= (dTheta * this.x) + (dPhi * this.y);
                  }

                  
                
if (clipTheta)
                  
this.displayTheta = Theta - dTheta * (ApproachRange - 1)
                
                
if (clipPhi)
                  
this.displayPhi = Phi - dPhi * (ApproachRange - 1)
                
                z 
= this.z
                
if (z < HitherPlane - CameraDistance)
                  
{
                    z 
= HitherPlane - CameraDistance
                  }


                DistanceScalar 
= CameraDistance / (z + CameraDistance)
                
                
this.distanceScalar = DistanceScalar
                
                
this.displayX = this.x * DistanceScalar
                
this.displayY = this.y * DistanceScalar
                
this.displayZ = DistanceScalar * FontScalar //The bigger z ,the smaller displayZ
              }

              
           
// Create bond between atom, strength affect the storke width
         function  Bond(Atom1, Atom2, Strength)
        
{
            
this.left = Atom1;
            
this.right = Atom2;
            
            
if (Strength != nullthis.strength = Strength;
            
            
this.SVG = SVGDocument.createElement("line");
            
this.SVG.setAttributeNS(null,"stroke""brown");
            
this.SVG.setAttributeNS(null,"stroke-width"this.strength);
            SVGDocument.getElementById(
"bonds").appendChild(this.SVG)
            
            Bonds[Bonds.length] 
= this;
            
            
this.refresh();
            
            
return this;
        }

        
        Bond.prototype.strength 
=   1 ;
        Bond.prototype.refresh 
=  Bond_Refresh;
            
        
// Update bond line according to atom positions
         function  Bond_Refresh()
        
{
             
            
var x1 = this.right.displayX;
            
var y1 = this.right.displayY;
            
var x2 = this.left.displayX;
            
var y2 = this.left.displayY;
            
            
this.SVG.setAttribute("x1", x1);
            
this.SVG.setAttribute("y1", y1);
            
this.SVG.setAttribute("x2", x2);
            
this.SVG.setAttribute("y2", y2);
        }

    
              
function  RefreshScreen()
              
{
                
for (var I = 0; I < Atoms.length; I++)
                  
{
                    Atoms[I].fixCoords()
                  }

                
                
for (var I = 0; I < Bonds.length; I++)
                  
{
                    Bonds[I].refresh();
                  }

                }


    
            
function  FixRange(x, y, z)
              
{
                
if (x < MinX) MinX = x;
                
if (x > MaxX) MaxX = x;
                
if (y < MinY) MinY = y;
                
if (y > MaxY) MaxY = y;
                
if (z < MinZ) MinZ = z;
                
if (z > MaxZ) MaxZ = z;
                
                XRange 
= MaxX - MinX
                YRange 
= MaxY - MinY
                ZRange 
= MaxZ - MinZ
                
                Range 
= max(max(XRange, YRange), ZRange)
                
                
if (Range == 0)
                  Range 
= Dimensions
                  
                Scale 
= Dimensions / Range
                
                XOffset 
= XRange / 2 - MaxX
                YOffset 
= YRange / 2 - MaxY
                ZOffset 
= ZRange / 2 - MaxZ
              }

              
              
              
            
function  max(a, b)
              
{
                
if (a > b)
                  
return a
                
else
                  
return b
              }

              
              
            
function  min(a, b)
              
{
                
if (a < b)
                  
return a
                
else
                  
return b
              }

              
              
            Mouse_LastX 
=   null
            Mouse_LastY 
=   null
            
            Mouse_dX 
=   null
            Mouse_dY 
=   null
            
              
            
function  MouseDown(MouseEvent)
              
{
                Mouse_LastX 
= MouseEvent.getScreenX()
                Mouse_LastY 
= MouseEvent.getScreenY()
                Mouse_dX    
= 0
                Mouse_dY    
= 0
                ThrownTheta 
= 0
                ThrownPhi   
= 0

                
if (!(Rotating))
                  
{
                    Rotating 
= true
                    Rotate()
                  }

              }

              
            
function  MouseMove(MouseEvent)
              
{
                
if ((Mouse_LastX != null&& (Mouse_LastY != null))
                  
{
                    Mouse_dX 
= ((1.0 - MouseMoveAverage) * Mouse_dX)
                            
+ (MouseMoveAverage * ((MouseEvent.getScreenX() - Mouse_LastX) * MouseScalar))
                    Mouse_dY 
= ((1.0 - MouseMoveAverage) * Mouse_dY)
                            
+ (MouseMoveAverage * ((MouseEvent.getScreenY() - Mouse_LastY) * MouseScalar))
                    
                    Theta 
-= Mouse_dX
                    Phi   
-= Mouse_dY
                    
                    Mouse_LastX 
= MouseEvent.getScreenX()
                    Mouse_LastY 
= MouseEvent.getScreenY()
                  }

              }

              
            
function  MouseUp(MouseEvent)
              
{
                
if ((Mouse_LastX != null&& (Mouse_LastY != null))
                  
{
                    ThrownTheta 
= Mouse_dX
                    ThrownPhi   
= Mouse_dY
                    
if ((ThrownTheta < MinMouseSpeed) && (ThrownTheta > (MinMouseSpeed * -1))
                     
&& (ThrownPhi   < MinMouseSpeed) && (ThrownPhi   > (MinMouseSpeed * -1)))
                    
{
                        ThrownTheta 
= 0
                        ThrownPhi   
= 0
                    }

                  }


                Mouse_LastX 
= null
                Mouse_LastY 
= null
              }

              
              
              
            
function  Rotate()
              
{
                MovementMade 
= false
             
                
for (var I = 0; I < Atoms.length; I++)
                  
{
                    JustMoved 
= Atoms[I].refresh()
                    MovementMade 
= MovementMade || JustMoved
                  }

                
                
for (var I = 0; I < Bonds.length; I++)
                  
{
                    Bonds[I].refresh();
                  }

                  
                Theta 
-= ThrownTheta
                Phi   
-= ThrownPhi

                
if ((MovementMade) || (ThrownTheta != 0|| (ThrownPhi != 0|| (Mouse_LastX != null))
                  setTimeout(
"window.rotate()"50)
                
else
                  Rotating 
= false
              }

              
            window.rotate 
=  Rotate
            
            
    
// Create molecule structure
     function  GenerateStructure()
    
{
        x 
= 10;
        y 
= 10;
        z 
= 10;
        
var point1 = new Atom(x, y, z, "url(#radial1)");
        
        x 
= 15;
        y 
= 10;
        z 
= 10;
        
var point2 = new Atom(x, y, z, "url(#radial2)");
        
        x 
= 10;
        y 
= 15;
        z 
= 10;
        
var point3 = new Atom(x, y, z, "url(#radial3)");
        
        x 
= 10;
        y 
= 10;
        z 
= 15;
        
var point4 = new Atom(x, y, z, "url(#radial4)");
        
        
        
var strength = 2;
        
        
var line1 = new Bond(point1, point2, strength);
        
var line2 = new Bond(point1, point3, strength);
        
var line3 = new Bond(point1, point4, strength);
        
        
        RefreshScreen();
        Rotate();
    }

  ]]
> script >
  
  
  
< defs >
    
< radialGradient id = " radial1 "  cx = " 75% "  cy = " 75% "  r = " 50% " >
        
< stop offset = " 15% "  stop - color = " white "   />
        
< stop offset = " 100% "  stop - color = " red "   />
    
radialGradient >
    
< radialGradient id = " radial2 "  cx = " 75% "  cy = " 75% "  r = " 50% " >
        
< stop offset = " 15% "  stop - color = " white "   />
        
< stop offset = " 100% "  stop - color = " green "   />
    
radialGradient >
    
< radialGradient id = " radial3 "  cx = " 75% "  cy = " 75% "  r = " 50% " >
        
< stop offset = " 15% "  stop - color = " white "   />
        
< stop offset = " 100% "  stop - color = " blue "   />
    
radialGradient >
    
< radialGradient id = " radial4 "  cx = " 75% "  cy = " 75% "  r = " 50% " >
        
< stop offset = " 15% "  stop - color = " white "   />
        
< stop offset = " 100% "  stop - color = " purple "   />
    
radialGradient >
    
    
    
< filter id = " dropShadow "  filterUnits = " userSpaceOnUse " >
         
< feOffset  in = " SourceAlpha "  dx = " 0 "  dy = " 85 "  result = " offset " />
         
< feGaussianBlur  in = " offset "  stdDeviation = " 5 "  result = " blur " />
         
< feMerge >
           
< feMergeNode  in = " blur " />
           
< feMergeNode  in = " SourceGraphic " />
         
feMerge >  
      
filter >

defs >

< g id = " backdrop " >
    
< rect x = " 0 "  y = " 0 "  width = " 350 "  height = " 300 "  style = " fill:white; "   />
g >

< g transform = " translate(-30, -85) " >
    
< g id = " structure " >
        
< g id = " shadows "  pointer - events = " none "  transform = " translate(200, 350) "  style = " fill:grey "   />
        
< g id = " bonds "  pointer - events = " none "  transform = " translate(200, 215) "   />
        
< g id = " atoms "  pointer - events = " none "  transform = " translate(200, 220) "  style = " text-anchor:middle;alignment-baseline:middle "   />
    
g >
g >

svg >



 
例子中都使用的是最简单的球体,而且缺省的处理只有一个固定的颜色渐变。
 
我认为SVG表现3D图形的问题主要有两个,一是SVG是否提供了必要的3D建模所需的一些工具,虽然SVG提供了相对丰富的坐标转换和渲染方法,例如滤镜效果,光照效果和高斯模糊,但这些操作的速度很慢,而且相对专门的3D工具,这些方法还远远不够,所以使用SVG建立3D图形意味着更多的代码和劳动。
 
二是效率问题,在例1中,圆形的数量到100个后,速度明显下降。在例二中,如果打开阴影特效,速度将慢的让人无法忍受。
 
因此,我认为在SVG中使用3D图形仅仅适用于以下情况:
 
1, 图形数量不多
2, 对图形效果的渲染要求不高或没有要求
3, 实时性,即对速度要求不高
 
期望使用SVG实现一个复杂的3D应用,例如3D射击游戏在现阶段是不现实的,但对一些特定应用,例如立体几何的教学软件等,SVG还是能够使用的。
对复杂的3D应用,除了OPENGL和DIRECTX等传统手段,可以期待X3D等其它XML应用的发展。
 

你可能感兴趣的:(SVG表现3D图形)