Comunidad orientada al desarrollo de videojuegos

Crear un nuevo Material

INTRODUCCION

En este tutorial aprenderemos como crear un nuevo material con “rendering” personalizado para usarlo en cualquier juego de Wave Engine.

PRIMEROS PASOS

Empezamos creando un nuevo proyecto de Wave Engine en Visual Studio. Añadimos una entidad a la escena para que nos sirva de prueba (en este ejemplo usaremos una esfera); de esta manera, veremos como actúa nuestro material cuando lo apliquemos.

Entity testShape = new Entity("TestShape")
        .AddComponent(new Transform3D())
        .AddComponent(Model.CreateSphere(5, 32))
        .AddComponent(new MaterialsMap(new MyMaterial("Content/DefaultTexture.wpk")))
        .AddComponent(new ModelRenderer());

Hemos creado deliberadamente la esfera con un mosaico más grande porque en el sombreado de la muestra vamos a realizar una transformación de desplazamiento y sin suficientes vértices se vería feo.

CREANDO EL MATERIAL

Agregar una nueva clase al proyecto llamada MyMaterial y hacer que herede de WaveEngine.Framework.Graphics.Material. Nos daremos cuenta que Visual Studio nos advierte que la clase debe implementar dos miembros, CurrentTechnique e Initialize. Estos miembros, junto con SetParameters, son obligatorios para cada material personalizado y vamos a explicar más adelante lo que hace cada uno.

DEFINIENDO LAS TÉCNICAS DEL “SHADER”

Vamos a empezar creando un array estático de objetos ShaderTechnique. Dentro de cada uno de ellos vamos a almacenar los parámetros que necesitará el adaptador para crear los objetos internos del shader: el nombre de la técnica, el vértice y los nombres del archivo pixel shader y una vertexFormat con el mismo diseño de la estructura de entrada del vertex shader.

private static ShaderTechnique[] techniques = 
        {
            new ShaderTechnique("MyMaterialTechnique",
                "MyMaterialvs",
                "MyMaterialps",
                VertexPositionNormalTexture.VertexFormat),
        };

De esta manera, cuando necesitemos inicializar cualquier Técnica, tendremos todas estas propiedades ya almacenadas para acceder a ellas fácilmente.

DECLARANDO LOS PARAMETROS DEL “SHADER”

Tenemos que crear una estructura que contendrá todos los parámetros que a los que accede shader. Desde que se asigna directamente a una memoria intermedia, tenemos que especificar su StructLayout como LayoutKind.Sequential, y debido a las limitaciones técnicas de DirectX, que debe tener un tamaño múltiplo de 16 bytes (aunque el tamaño total de los miembros incluidos puede ser inferior a eso). A continuación, declaramos una variable de miembro privado que contendrá una instancia de la estructura que contiene los parámetros.

[StructLayout(LayoutKind.Sequential, Size = 16)]
private struct MyMaterialParameters
{
    public float Time;
}
private MyMaterialParameters shaderParameters = new MyMaterialParameters();

AÑADIENDO UN MAPA DE TEXTURA

Como este material de ejemplo muestra cómo se realiza la asignación de texturas, vamos a necesitar una propiedad que almacene un identificador para un objeto Texture existente. Entonces, hay dos maneras de construir el material con la textura especificada:

  • Pasar un objeto Texture como parámetro al constructor.
  • Pasar una cadena que contiene la ruta del asset y el material se carga cuando sea necesario.

Vamos a ilustrar el segundo método, por lo que añadiremos una cadena y una propiedad Texture:

private string diffuseMapPath;

public Texture DiffuseMap
{
    get;
    set;
}

Después mostraré como se inicializan.

SELECCIONANDO LA TÉCNICA APROPIADA

Recordamos el campo CurrentTechnique que se ha mencionado anteriormente? Se trata de un campo de sólo lectura que devuelve el nombre de la técnica del shader que se debe utilizar al dibujar en función de cómo esté configurado el material (¿está la iluminación permitida? ¿Debo dibujar con una textura?). Dado que este material de la muestra sólo tiene una técnica, esto va a ser bastante sencillo:

public override string CurrentTechnique
{
    get { return techniques[0].Name; }
}

PREPARANDO EL CONSTRUCTOR

El constructor del material personalizado se encarga de modificar los valores por defecto del material. Los únicos requisitos que siempre hay que cumplir son la asignación de la instancia privada de la estructura que contiene los parámetros a la propiedad Parameters. Esto se hace porque DirectX puede asignar correctamente el diseño de la estructura para sus buffers internos al crear el objeto shader. Después de esto, se puede llamar de forma segura a InitializeTechniques pasando el array de ShaderTechnique previamente definido como el único parámetro:

public MyMaterial(string diffuseMap)
    : base(DefaultLayers.Opaque)
{
    this.diffuseMapPath = diffuseMap;
    this.Parameters = this.shaderParameters;

    this.InitializeTechniques(techniques);
}

INICIALIZANDO LOS “ASSETS” ESPECÍFICOS

La función Initialize se encarga de modificar cualquier miembro que no se podía hacer en el constructor, por ejemplo, los assets de la textura necesitan ser cargados en un AssetsContainer para gestionar adecuadamente su vida. Vamos a cargar aquí la textura que se necesita para nuestro shader:

public override void Initialize(AssetsContainer assets)
{
    try
    {
        this.DiffuseMap = assets.LoadAsset<Texture2D>(this.diffuseMapPath);
    }
    catch (Exception e)
    {
        throw new InvalidOperationException("MyMaterial needs a valid texture.");
    }
}

PASAR PARÁMETROS AL “SHADER”

La función SetParameters pasa cualquier dato necesario al shader. Debemos llevar a cabo la acciones en este orden específico:

  • Llamar a base.SetParameters
  • Cambiar los parámetros del shader en la instancia de estructura privada creada con anterioridad (en este caso, shaderParameters).
  • Asignar la instancia de estructura al campo Parameters.
  • Establecer las texturas que deseamos utilizar en las ranuras de la textura correctas.
public override void SetParameters()
{
    base.SetParameters();

    this.shaderParameters.Time = (float)DateTime.Now.TimeOfDay.TotalSeconds;

    this.Parameters = shaderParameters;

    this.graphicsDevice.SetTexture(this.DiffuseMap, 0);
}

ESCRIBIENDO EL “SHADER” DE DirectX

Ahora que tenemos todo el código para usar nuestro material dentro de Wave, tenemos que escribir los shaders que serán llamados. Vamos a comenzar con el DirectX.

Comenzamos agregando una carpeta Shaders en el proyecto que contiene la clase de material. Esto es obligatorio como los nombres de directorios que están codificados dentro de la lógica de manejo de materiales de Wave. Dentro de este directorio, creamos una llamada HLSL y otra llamada GLSL. Ambos directorios contendrán tantas carpetas como los materiales que estamos creando, con el mismo nombre que la clase del material.

En cuanto al shader HLSL, creamos un nuevo archivo .fx donde se escribirá el shader. Este archivo sólo se utiliza como un paso intermedio porque Wave necesita el shader en forma binaria, por lo que lo nombramos como queramos – vamos a utilizar MyMaterial.fx.

Comenzamos agregando el siguiente código:

cbuffer Matrices : register(b0)
{
    float4x4    WorldViewProj                        : packoffset(c0);
    float4x4    World                                : packoffset(c4);
    float4x4    WorldInverseTranspose                : packoffset(c8);
};

Este búfer es obligatorio para todos los shaders, ya que contiene las matrices asignadas automáticamente por Wave. Si necesitamos adicionales, se pueden pasar en el Parameters del búfer.

cbuffer Parameters : register(b1)
{
    float                Time                : packoffset(c0.x);
};

Este es el búfer que asigna los parámetros personalizados que se pasan al shader. Recuerdamos que debemos colocarlos en el orden apropiado y el uso de la directiva packoffset según sea necesario.

Texture2D DiffuseTexture             : register(t0);
SamplerState DiffuseTextureSampler     : register(s0);

El mapa de textura y sampler se asignan a la misma ranura que hemos especificado a través del código, en este caso, la primera.

struct VS_IN
{
    float4 Position : POSITION;
    float3 Normal    : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VS_OUT
{
    float4 Position : SV_POSITION;
    float2 TexCoord : TEXCOORD0;
};

Comprobamos que la estructura de entrada de los vértices del shader coinciden con el formato de vértice especificado en la declaración de la técnica del shader – en este caso, VertexPositionNormalTexture.

Ahora, vamos a proceder a escribir las funciones de vértices y pixel shader. El vertex shader aplicará una deformación sinusoidal sencilla basada en el parámetro Time, y el pixel shader tomará muestras de la textura y las asignará a la superficie:

VS_OUT vsMyMaterial( VS_IN input )
{
    VS_OUT output = (VS_OUT)0;

    float offsetScale = abs(sin(Time + (input.TexCoord.y * 16.0))) * 0.25;
    float4 vectorOffset = float4(input.Normal, 0) * offsetScale;
    output.Position = mul(input.Position + vectorOffset, WorldViewProj);
    output.TexCoord = input.TexCoord;

    return output;
}

float4 psMyMaterial( VS_OUT input ) : SV_Target0
{
    return DiffuseTexture.Sample(DiffuseTextureSampler, input.TexCoord);
}

COMPILANDO EL “SHADER” DE DIRECTX

Desde los shaders HLSL es necesario que sea compilado, vamos a utilizar la herramienta fxc.exe para esto. Si estamos en Windows 7, tendremos que instalar el último SDK de DirectX, y lo podemos encontrar en el directorio Utilities\bin\x86. Si estamos en Windows 8, lo que necesitamos es instalar el Windows 8 SDK y encontrar la herramienta en el directorio Kits\8.0\bin\x86.

Ahora, recordemos que debemos compilar los shaders con el Shader Model 4.0 – DirectX 9.1 nivel del perfil objetivo con lo que el mismo shader puede ser utilizado a través de Windows, Windows Phone y Windows Store:

fxc.exe /nologo MyMaterial.fx /T vs_4_0_level_9_1 /E vsMyMaterial /Fo MyMaterialvs.fxo
fxc.exe /nologo MyMaterial.fx /T ps_4_0_level_9_1 /E psMyMaterial /Fo MyMaterialps.fxo

Agregamos los archivos de salida en el directorio HLSL/MyMaterial del proyecto y recordamos poner el Build Action como Embedded Resource. Ahora ejecutamos y si no hay errores podremos ver algo como esto:

:imagen01

ESCRIBIENDO EL SHADER DE OpenGL

Ahora, es el momento de añadir el shader para plataformas OpenGL. Añade los archivos del vértice (MyMaterialvs.vert) y el fragmento (MyMaterialps.frag) a la carpeta GLSL/MyMaterial y escribimos el código en ellos. Recordemos poner su Build Action en Embedded Resource.

uniform mat4    WorldViewProj;

uniform float    Time;

attribute vec3 Position0;
attribute vec3 Normal0;
attribute vec2 TextureCoordinate0;

varying vec2 outTexCoord;

void main(void)
{
    float offsetScale = abs(sin(Time + (TextureCoordinate0.y * 16.0))) * 0.25;
    vec3 vectorOffset = Normal0 * offsetScale;
    gl_Position = WorldViewProj * vec4(Position0 + vectorOffset, 1);
    outTexCoord = TextureCoordinate0;
}

Tengamos en cuenta que, dado que OpenGL no tiene soporte para búferes variables en este momento, los parámetros del shader están especificados como variables uniformes y el formato del búfer de vértices como atributos. Aparte de eso, el shader es muy similar a la realizado en HLSL.

#ifdef GL_ES
precision mediump float;
#endif

uniform sampler2D Texture;

varying vec2 outTexCoord;

void main(void)
{
    gl_FragColor = texture2D(Texture, outTexCoord);
}

Ahora, hay un problema con la forma en que las  texturas funcionan en la versión OpenGL: asegurémonos de que el llamamos a nuestro uniform sampler2D con el mismo nombre que la propiedad de la textura que se establece en la clase MyMaterial. De lo contrario, podrían ocurrir errores inesperados.

Ahora, convertimos el proyecto para Android y ejecutamos. En caso de que haya errores de compilación en los nuevos shaders, estos van a ser escritos en la ventana de resultados de Visual Studio.

capturaAndroid

FUENTES

Tutorial original en inglés: http://blog.waveengine.net

Para descargar Wave Engine: http://www.waveengine.net

Traducción por Carlos Sánchez López

, , , , , , ,

Leave a Reply