Search this blog

16 December, 2010

Shameless plug

Can't help being proud of this game. At the end of Round 4 I felt like doing more was going to be hard, especially as we optimized the shit out of it and we were "done" in terms of the amount of computation we could push on this generation. Champion manages to do much more without computing much more, while also handling a lot more content. It was mostly an achievement of hygiene and understanding rather than optimization and complex techniques. That also left plenty of space for the future...

Round 4

Champion


http://www.gametrailers.com/video/debut-trailer-fight-night/707453

http://www.ign.com/videos/2010/12/21/fight-night-champion-champion-mode-trailer?objectid=81346&show=HD

15 December, 2010

Stupid sample generator - 3d version

//  Generates a packing of points inside a shape
//  press right/middle mouse buttons a few times until convergence...
class pnt
{
  float x; float y; float z;
  
  float len()
  {
    return sqrt(x*x + y*y + z*z);
  }
};
pnt points[] = new pnt[32];
float scalefact = 100;
float mindist = scalefact * 0.5;
void setup() {
  size(512, 512, P3D);
  fill(204);
  
    for(int i=0; i< points.length; i++)
  {
    points[i]=new pnt();
    points[i].x = random(scalefact*2)-scalefact;
    points[i].y = random(scalefact*2)-scalefact;
    points[i].z = random(scalefact*2)-scalefact;
  }
}
float getImportance(pnt a)
{
  return 1.3 - (((a.z/scalefact) + (a.z/a.len()))/2);
}
void mousePressed() 
{
  if(mouseButton == LEFT) // dump points, report
  {  
    final float UNIT_HEMISPHERE_VOL = (4.0/3.0) * 3.141592 * 0.5;
    float totalVol = 0;
    
    for(int i=0; i< points.length; i++)
    {
      float normRadius = mindist * getImportance(points[i]) / scalefact;
      float vol = (4.0/3.0) * 3.141592 * normRadius * normRadius * normRadius;
      totalVol+=vol;
    }
    println("vol ratio" + UNIT_HEMISPHERE_VOL/totalVol);
    
    for(int i=0; i< points.length; i++)
    {
      //println(points[i].x/scalefact+", "+points[i].y/scalefact+", "+points[i].z/scalefact+", "+points[i].len()/scalefact+",");
      
      float normRadius = mindist * getImportance(points[i]) / scalefact;
      float vol = (4.0/3.0) * 3.141592 * normRadius * normRadius * normRadius;
      float weight = vol / totalVol;
      
      println(points[i].x/scalefact+", "+points[i].y/scalefact+", "+points[i].z/scalefact+", "+weight+",");
    }
    println("vol ratio: " + UNIT_HEMISPHERE_VOL/totalVol);
  }
  else if(mouseButton == CENTER)
  {
    mindist -= 5; // manual control...
  }
  else // RIGHT mouse button
  {
    // A LITTLE BIT OF NOISE
    for(int i=0; i< points.length; i++)
    {
      points[i].x += random(mindist/(scalefact/10))-(mindist/(scalefact/5));
      points[i].y += random(mindist/(scalefact/10))-(mindist/(scalefact/5));
      points[i].z += random(mindist/(scalefact/10))-(mindist/(scalefact/5));
    }
  }
}
void draw() {
  lights();
  background(0);
  
  // Change height of the camera with mouseY
  camera(sin(mouseX/512.0 * 6.0)*300, cos(mouseX/512.0 * 6.0)*300, mouseY, // eyeX, eyeY, eyeZ
         0.0, 0.0, 0.0, // centerX, centerY, centerZ
         0.0, 0.0, -1.0); // upX, upY, upZ
  
  noStroke();
  
  // SHAPE CONSTRAINT
  for(int i=0; i< points.length; i++)
  {    
    float len = points[i].len();
    
    float radius = getImportance(points[i])*mindist;
    
    len+=radius;
    
    if(len > scalefact) // inside the sphere
    {
      points[i].x /= len / scalefact;
      points[i].y /= len / scalefact;
      points[i].z /= len / scalefact;
    }
    // hemisphere
    if(points[i].z-radius< 0) points[i].z=radius;
  }
  float totaldist = 0;
  // DISTANCE CONSTRAINT
  for(int i=0; i< points.length; i++)
  {       
    float radius = getImportance(points[i])*mindist;
    
    for(int k=0; k< 50; k++)
    {
      int j = (int)random(points.length);
      
      if(i!=j)
      {
        pnt tmp = new pnt();
        
        tmp.x = points[j].x - points[i].x;
        tmp.y = points[j].y - points[i].y;
        tmp.z = points[j].z - points[i].z;
        
        float len = tmp.len();
        
        if( len <  radius )
        {
          tmp.x = (tmp.x/len) * radius;
          tmp.y = (tmp.y/len) * radius;
          tmp.z = (tmp.z/len) * radius;
          
          points[i].x = points[j].x - tmp.x;
          points[i].y = points[j].y - tmp.y;
          points[i].z = points[j].z - tmp.z;
          
          totaldist += len;
        }
      }
    }
  }
  
  // display distance violation
  fill(15,15,15,255);
  box(scalefact,scalefact,1);
  
  totaldist = sqrt(totaldist);
  fill(128,0,128,128);
  box(totaldist,totaldist,5);
    
  for(int i=0; i< points.length; i++)
  {
      pushMatrix();
      translate(points[i].x,points[i].y,points[i].z);
      
      if(mindist>0)
      {
        fill(255,255,255,64);
        sphere(mindist * getImportance(points[i]) );
      }
      
      fill(255,0,0,255);
      sphere(5 * getImportance(points[i]) );
      popMatrix();
      stroke(255,0,0,255);     
      line(0,0,0,points[i].x,points[i].y,points[i].z);
      noStroke();
  }
}

Stupid sample generator

I had to write in a rush some code to generate low-discrepancy samples in a shape (i.e. hemisphere). For those kind of quick hacks I find processing to be great, mostly because of its visual nature.



The code is really stupid and it requires some manual interaction. With the left mouse button you dump the sample info, with the middle you shrink the sample minimum distance spheres, with the right you add a random shake to the whole thing (yes, it's laughable...).

Anyways. Here it is.




//  Generates a packing of points inside a 2d shape
//  press right/middle mouse buttons a few times until convergence...
class pnt
{
  float x; float y;
};


pnt points[] = new pnt[16];
float mindist = 60;


void setup() {
  size(512, 512, P3D);
  fill(204);
  
    for(int i=0; i< points.length; i++)
  {
    points[i]=new pnt();
    points[i].x = random(200)-100;
    points[i].y = random(200)-100;
  }
}


void mousePressed() 
{
  if(mouseButton == LEFT)
  {
    for(int i=0; i< points.length; i++)
    {
      float len = sqrt(points[i].x*points[i].x + points[i].y*points[i].y);
      println(points[i].x/100.0 + ", " + points[i].y/100.0 + ", " + len/100.0 + ",");
    }
  }
  else if(mouseButton == CENTER)
  {
    mindist -= 5; // manual control...
  }
  else // RIGHT mouse button
  {
    // A LITTLE BIT OF NOISE
    for(int i=0; i< points.length; i++)
    {
      points[i].x += random(mindist/10)-(mindist/20);
      points[i].y += random(mindist/10)-(mindist/20);
    }
  }
}


void draw() {
  lights();
  background(0);
  
  // Change height of the camera with mouseY
  camera(30.0, mouseY, 400.0, // eyeX, eyeY, eyeZ
         0.0, 0.0, 0.0, // centerX, centerY, centerZ
         0.0, 1.0, 0.0); // upX, upY, upZ
  
  noStroke();


  float totaldist = 0;


  // DISTANCE CONSTRAINT
  for(int i=0; i< points.length; i++)
  {       
    for(int k=0; k<50; k++)
    {
      int j = (int)random(points.length);
      if(i!=j)
      {
        pnt tmp = new pnt();
        
        tmp.x = points[j].x - points[i].x;
        tmp.y = points[j].y - points[i].y;
        
        float len = sqrt(tmp.x*tmp.x + tmp.y*tmp.y);
        
        if(len< mindist)
        {
          tmp.x = (tmp.x/len)*mindist;
          tmp.y = (tmp.y/len)*mindist;


          points[i].x = points[j].x - tmp.x;
          points[i].y = points[j].y - tmp.y;


          
          totaldist += len;
        }
      }
    }
  }
  
  // DISTANCE FROM ZERO CONSTRAINT
  for(int i=0; i< points.length; i++)
  {       
      float len = sqrt(points[i].x*points[i].x + points[i].y*points[i].y);
      
      if(len< mindist)
      {        
        points[i].x /= len/(mindist);
        points[i].y /= len/(mindist);
      }
  }
  
  // SHAPE CONSTRAINT
  for(int i=0; i< points.length; i++)
  {    
    float len = sqrt(points[i].x*points[i].x + points[i].y*points[i].y);
    
    if(len>100)
    {
      points[i].x/=len/100.0;
      points[i].y/=len/100.0;
    }
  }
  
  // display distance violation
  totaldist = sqrt(totaldist);
  fill(15,15,15,255);
  box(totaldist);
    
  for(int i=0; i< points.length; i++)
  {
      pushMatrix();
      translate(points[i].x,points[i].y,0);
      if(mindist>0)
      {
        fill(255,255,255,64);
        sphere(mindist);
      }
      fill(255,0,0,255);
      sphere(5);           
      popMatrix();
  }
}




05 December, 2010

Leaving EA

I've always avoided mentioning the company I've been working with while writing this blog. Not that it was a secret, actually many people in my company knew the blog, it's just that I don't love the idea of linking the blog to a company as I don't want to be bothered by people asking questions on the blog about my work. 

Recently though I've decided to resign and accept a new offer, so now I'm free to write about my last three years of work. 

I'll publish also some photos, I have to admit that for the first few days when I started working there I could not avoid smiling while walking around in the amazing Burnaby's campus.

My cube, while working for Fight Night Champion
I've been very lucky there. When I left Milan for EA Canada, most of my friends in the industry were telling me that in such a big company I was going to end up taking care of a bunch of small tasks, not being able to influence much such huge and bureaucratic teams.
Two of the three building of EA Canada's studio
Bullshit! It turns out that to make great games you need great teams, able to move quickly, experiment new ideas, be agile and open to all the relevant opinions. I actually was able to do more research in my job as a on-team rendering engineer than my previous two years, in a smaller company, where I was a R&D engineer working on their next-gen graphics engine!
Secondary entrance, at night
It is true that not all the games at EA are the same, and that you can be moved around regardless of your preferences. But if you're smart, if you fight for it, and if you're not too unlucky, you'll end up being very happy about your job.
One of the cafeterias
It's not easy, and there is a lot to learn about how such a huge company works, but it can be an unique experience. Surely there are some opportunities that you won't find anywhere else...
Study rooms area
One of the best points for me was the ability of learning, talking, and sharing knowledge with such a huge worldwide audience.
Technology sharing at EA is somewhat a recent process, and it has its problems (you have a lot of nicely packaged, great tech, but there is not much overall design as you put together a game made of lots of different bits that came from all over the world) but it doesn't matter.
One of the many patios
The best asset is not the technology, is the knowledge! Even if EA has some stupid legal restrictions and there is still a lot of work to be done to let every game influence each other better and in a more effective way (I tried to do my part, for what I could, to address that), you won't easily find another company where you can access so much data and papers about virtually anything. Or where you can so easily talk with the best individuals across the world in any game-related field, work with universities, or directly with the hardware manufacturers.
Corridor of the second building

Why I've left? Because even if you can love your job, and love your team, hardly you can have such an empathy for the whole company. EA is a big corporation, it has its own objectives, mostly driven by the market. That is fine, and I don't mind the fact that it can move people around or fire them faster than lightspeed, but it means that the relationship with it has to be strictly a business one. It's not a family, it won't care much about you, so in my opinion it's hard to develop any emotional attachment to it. For a couple of years it failed to match what I believed to be my market value, so I ended up signing a more lucrative contract, it's as simple as that.

Fight Night Champion, WIP screenshot

Fight Night Champion has been my last game at EAC. It's a game that I feel particularly "mine", and I hope it will perform great! It surely has all the potential to be 2/3 metacritic points above Round 4 (that was 87/88) but who knows, reviews are influenced by many factors other than quality (i.e. hype...). We'll see, I surely think the team did a great job.

02 November, 2010

Stereoscopic test

Yesterday I did a small test with stereo rendering, trying to reproject the image from one eye to the other. It's very crude but I wanted to show the "theory" or the basic idea of the procedure as I got a few people asking me about it.

Stereo rendering (standard red/cyan glasses needed)

Of course the most interesting thing is how to correctly inpaint the holes in your visibility. This is a very general problem that also applies to other post effects like motion blur and depth of field blur, the solution I use in this test well... I won't even call it a solution. 

Much better methods have to be used for this to work decently, but that will (might...) be the topic of another post!

Reconstructed view
Rendered view




 float Script : STANDARDSGLOBAL <
    string UIWidget = "none";
    string ScriptClass = "scene";
    string ScriptOrder = "standard";
    string ScriptOutput = "color";
    string Script = "Technique=Main;";
> = 0.8; // version #

// -- Untweakables

float4x4 WorldViewProj : WorldViewProjection < string UIWidget="None"; >;
float4x4 WorldView : WorldView < string UIWidget="None"; >;
float4x4 Proj : Projection ;

float2 BufferSize : ViewportPixelSize < string UIWidget="None"; >;

float4 ClearParam < string UIWidget="None"; > = {0,0,0,0};

// -- Buffers and samplers

texture Bake : RENDERCOLORTARGET
<
    float2 ViewPortRatio = {1,1};
    string Format = "A8B8G8R8";
    string UIWidget = "None";
>;
sampler2D BakeSampler = sampler_state
{
    texture = ;
    MagFilter = Point;
    MinFilter = Point;
    AddressU = Clamp;
    AddressV = Clamp;
    MipFilter = Point;
};

// -- Data structures

struct GeomVS_Out
{
    float4 Pos : POSITION;
    float4 PosCopy : TEXCOORD0;
    float2 UV : TEXCOORD2;
};

struct FSQuadVS_InOut // fullscreen quad
{
    float4 Pos : POSITION;
    float2 UV : TEXCOORD0;
};

// -- Shaders

GeomVS_Out GeomVS(float4 pos : POSITION, float2 uv : TEXCOORD0)
{
    GeomVS_Out Out;

    Out.Pos = mul( pos, WorldViewProj );
    Out.PosCopy = Out.Pos;
    Out.UV = uv;
   
    return Out;
}

FSQuadVS_InOut FSQuadVS(FSQuadVS_InOut In)
{   
    return In;
}

// FX Composer does not pass near and far planes via a semantic, we have to "extract" them
float2 GetPlanesFromDxProjection()
{
    float near = -Proj[3][2] / Proj[2][2];
    float far = -Proj[3][2] / (Proj[2][2] - 1.0);
   
    return float2(near, far);
}

float4 EncUnitRangeToRGBA8( float v )
{
    float4 enc = float4(1.0, 255.0, 65025.0, 160581375.0) * v;
   
    enc = frac(enc);
    enc -= enc.yzww * float4(1.0/255.0, 1.0/255.0, 1.0/255.0, 0.0);
   
    return enc;
}

float DecUnitRangeToRGBA8( float4 rgba )
{
    return dot( rgba, float4(1.0, 1/255.0, 1/65025.0, 1/160581375.0) );
}

float4 BakePS(GeomVS_Out In) : COLOR
{
    // 0 to 1 from near to far
    float linearDepth = In.PosCopy.z / GetPlanesFromDxProjection().y;
   
    float testColor = dot(frac(In.UV.xy * 10) ,0.5.xx) + 0.25;
   
    return float4(EncUnitRangeToRGBA8(linearDepth).xyz, testColor);
}

float4 DecodeBake(float2 uv)
{
    float4 sample = tex2D(BakeSampler, uv);
    float linearZ = DecUnitRangeToRGBA8(float4(sample.xyz,0));

    // back to viewspace...
    float2 nearFar = GetPlanesFromDxProjection();
    linearZ = linearZ * (nearFar.y-nearFar.x) + nearFar.x;

#define VIEWTOUVMULT (float2(Proj[0][0], -Proj[1][1]) * 0.5)
#define VIEWTOUVADD float2(0.5, 0.5)   
    float2 xy = ((uv - VIEWTOUVADD) * linearZ)/VIEWTOUVMULT;
   
    return float4(xy, linearZ, sample.w); // view xyz, pattern
}

float4 CopyPS(FSQuadVS_InOut In) : COLOR
{
    return float4(DecodeBake(In.UV).zzz / 5, 1);
    return float4(DecodeBake(In.UV).www, 1);
}

float3 ReprojectView(float3 viewPos)
{
    // Here we should go from the view position of the baked eye to the one of the other
    // it can be pretty generic but it assumes the difference will be only along the
    // screen X axis...
   
    return viewPos + float3(0.2,0,0);
}

float4 ReprojectPS(FSQuadVS_InOut In) : COLOR
{
    float reprojDepth = 9999;
    float reprojColor = 0;
   
    float nearestColor = 0;
    float nearestDistance = 9999;
   
    float4 decodedCenter = DecodeBake(In.UV); // xyz = view, w = test pattern
   
    float4 projectedCenter = mul(float4(decodedCenter.xyz,1), Proj);
    projectedCenter /= projectedCenter.w;
   
    for(float i=-63; i<64; i++) // note: brute force = bad, just a test
    {
        float2 sampleUV = In.UV + float2(i / BufferSize.x, 0);
       
        // we assume in this test that the "color" is baked in w
        // also, we assume that the scene fills the entire screen,
        // we draw something everywhere...
        float4 decoded = DecodeBake(sampleUV);   
       
        float3 newView = ReprojectView(decoded.xyz);
       
        float4 projectedNewView = mul(float4(newView,1), Proj);
        projectedNewView /= projectedNewView.w;
       
        float rowDist = abs(projectedNewView.x - projectedCenter.x);
       
        // Check if the new sample covers our pixel
        // note: al these tests can be made "fuzzy" by constructing
        // a weighting function instead
        if(rowDist < (2.0 / BufferSize.x))
        {
            if(reprojDepth>newView.z)
            {
                reprojColor = decoded.w;
                reprojDepth = newView.z;
            }
        }
       
        // We'll use this to fill the holes if any
        // note: this is a very crude inpaiting method, something
        // way better should be used for the method to work decently
        float heuristicDistance = rowDist*rowDist / newView.z;
        if(heuristicDistance < nearestDistance)
        {
            nearestColor = decoded.w;
            nearestDistance = heuristicDistance;
        }
    }
   
    // If we didn't find anything, we pull something from thin air...
    if(reprojColor==0) reprojColor = nearestColor;

    //return float4(decodedCenter.www,1);
    //return float4(reprojColor.xxx,1);
    return float4(reprojColor,decodedCenter.ww,1);
}

// -- Techniques

technique Main
<
    string Script =
        "Pass=Bake;"
        "Pass=FullScreen;"
    ;
>
{
    pass Bake
    <
        string Script =
            "RenderColorTarget0=Bake;"
            "ClearSetColor=ClearParam;"
            "Clear=Color;"
            "Clear=Depth;"
            "Draw=Geometry;";
    >
    {
        ZEnable = true;
        ZWriteEnable = true;
       
        VertexShader = compile vs_3_0 GeomVS();
        PixelShader = compile ps_3_0 BakePS();
    }
   
    pass FullScreen
    <
        string Script =
            "RenderColorTarget0=;"
            "Draw=Buffer;";
    >
    {
        ZEnable = false;
        ZWriteEnable = false;
   
        VertexShader = compile vs_3_0 FSQuadVS();
        PixelShader = compile ps_3_0 ReprojectPS(); //CopyPS();
    }
};