Bagaimana saya melakukan filter xBR atau hqx di XNA?


11

Saya ingin membuat permainan saya ditingkatkan dengan salah satu filter hqx (hq2x, hq3x atau hq4x) atau filter xBR di shader.

Bagaimana saya bisa melakukan ini di XNA 4.0 dan SM3?

Catatan: pertanyaan ini telah diedit untuk menjadi seperti sekarang.


Pertanyaan yang menarik, mungkin pengambilan sampel titik dengan filter pasca-proses FXAA yang kasar akan melakukan hal yang serupa ... Hanya gagasan singkat, saya belum mengujinya.
János Turánszki

en.wikipedia.org/wiki/Hqx menjelaskan kira-kira cara kerjanya dan memiliki beberapa tautan ke implementasi.
Adam


Saya mendapatkan versi retas yang bekerja dengan menggunakan hqxSharpproyek, tetapi sial itu lambat (yang diperingatkan). Saya butuh sesuatu yang bisa menjaga framerate yang layak.
Tes

1
Juga saya pikir CG kompatibel dengan DirectX 9 yang berbasiskan iirc XNA. Coba kompilasi salah satu contoh di tautan seolah-olah itu file HLSL. github.com/libretro/common-shaders/tree/master/hqx
ClassicThunder

Jawaban:


6

Anda dapat mengurangi jumlah instruksi dengan menggunakan operasi vektor: mis. Alih-alih

edr = bool4((w1.x < w2.x) && ir_lv1.x, 
            (w1.y < w2.y) && ir_lv1.y, 
            (w1.z < w2.z) && ir_lv1.z, 
            (w1.w < w2.w) && ir_lv1.w);

kamu bisa menulis

edr = (w1 < w2) && ir_lv1;

Operator di HLSL dapat diterapkan ke vektor, bahkan yang logis suka &&dua bool3nilai. Operator-operator ini akan melakukan operasi secara komponen.

Kode shader

float2 texture_size;
float4x4 matrixTransform;

const static float coef = 2.0;
const static float3 yuv_weighted = float3(14.352, 28.176, 5.472);

sampler decal : register(s0);

float4 df(float4 A, float4 B)
{

    // begin optimization: reduction of 42 instruction slots
    float4 result = float4(A.x - B.x, A.y - B.y, A.z - B.z, A.w - B.w);

    return abs(result);
    // end optimization

    /* old code 

    //return float4(abs(A.x - B.x), abs(A.y - B.y), abs(A.z - B.z), abs(A.w - B.w));
    */
}

float4 weighted_distance(float4 a, float4 b, float4 c, float4 d, float4 e, float4 f, float4 g, float4 h)
{
    return (df(a, b) + df(a, c) + df(d, e) + df(d, f) + 4.0 * df(g, h));
}

float4 main_vertex(inout float2 texCoord : TEXCOORD0, inout float4 position : SV_Position) : TEXCOORD1
{
    float2 ps = float2(1.0 / texture_size.x, 1.0 / texture_size.y);
    float4 t1;

    t1.xy = float2(ps.x, 0); // F
    t1.zw = float2(0, ps.y); // H

    position = mul(position, matrixTransform);

    return t1;
}

/*    FRAGMENT SHADER    */
float4 main_fragment(float4 p : POSITION0, float2 tex0 : TEXCOORD0, float4 tex1 : TEXCOORD1) : COLOR0
{
    bool4 edr, edr_left, edr_up, px; // px = pixel, edr = edge detection rule
    bool4 ir_lv1, ir_lv2_left, ir_lv2_up;
    bool4 nc; // new_color
    bool4 fx, fx_left, fx_up; // inequations of straight lines.

    float2 fp = frac(tex0 * texture_size);
    float2 dx = tex1.xy;
    float2 dy = tex1.zw;

    float3 A = tex2D(decal, tex0 - dx - dy).xyz;
    float3 B = tex2D(decal, tex0 - dy).xyz;
    float3 C = tex2D(decal, tex0 + dx - dy).xyz;
    float3 D = tex2D(decal, tex0 - dx).xyz;
    float3 E = tex2D(decal, tex0).xyz;
    float3 F = tex2D(decal, tex0 + dx).xyz;
    float3 G = tex2D(decal, tex0 - dx + dy).xyz;
    float3 H = tex2D(decal, tex0 + dy).xyz;
    float3 I = tex2D(decal, tex0 + dx + dy).xyz;
    float3 A1 = tex2D(decal, tex0 - dx - 2.0*dy).xyz;
    float3 C1 = tex2D(decal, tex0 + dx - 2.0*dy).xyz;
    float3 A0 = tex2D(decal, tex0 - 2.0*dx - dy).xyz;
    float3 G0 = tex2D(decal, tex0 - 2.0*dx + dy).xyz;
    float3 C4 = tex2D(decal, tex0 + 2.0*dx - dy).xyz;
    float3 I4 = tex2D(decal, tex0 + 2.0*dx + dy).xyz;
    float3 G5 = tex2D(decal, tex0 - dx + 2.0*dy).xyz;
    float3 I5 = tex2D(decal, tex0 + dx + 2.0*dy).xyz;
    float3 B1 = tex2D(decal, tex0 - 2.0*dy).xyz;
    float3 D0 = tex2D(decal, tex0 - 2.0*dx).xyz;
    float3 H5 = tex2D(decal, tex0 + 2.0*dy).xyz;
    float3 F4 = tex2D(decal, tex0 + 2.0*dx).xyz;

    float4 b = mul(float4x3(B, D, H, F), yuv_weighted);
    float4 c = mul(float4x3(C, A, G, I), yuv_weighted);
    float4 e = mul(float4x3(E, E, E, E), yuv_weighted);
    float4 d = b.yzwx;
    float4 f = b.wxyz;
    float4 g = c.zwxy;
    float4 h = b.zwxy;
    float4 i = c.wxyz;

    float4 i4 = mul(float4x3(I4, C1, A0, G5), yuv_weighted);
    float4 i5 = mul(float4x3(I5, C4, A1, G0), yuv_weighted);
    float4 h5 = mul(float4x3(H5, F4, B1, D0), yuv_weighted);
    float4 f4 = h5.yzwx;

    float4 Ao = float4(1.0, -1.0, -1.0, 1.0);
    float4 Bo = float4(1.0, 1.0, -1.0, -1.0);
    float4 Co = float4(1.5, 0.5, -0.5, 0.5);
    float4 Ax = float4(1.0, -1.0, -1.0, 1.0);
    float4 Bx = float4(0.5, 2.0, -0.5, -2.0);
    float4 Cx = float4(1.0, 1.0, -0.5, 0.0);
    float4 Ay = float4(1.0, -1.0, -1.0, 1.0);
    float4 By = float4(2.0, 0.5, -2.0, -0.5);
    float4 Cy = float4(2.0, 0.0, -1.0, 0.5);

    // These inequations define the line below which interpolation occurs.
    fx.x = (Ao.x*fp.y + Bo.x*fp.x > Co.x);
    fx_left.x = (Ax.x*fp.y + Bx.x*fp.x > Cx.x);
    fx_up.x = (Ay.x*fp.y + By.x*fp.x > Cy.x);

    fx.y = (Ao.y*fp.y + Bo.y*fp.x > Co.y);
    fx_left.y = (Ax.y*fp.y + Bx.y*fp.x > Cx.y);
    fx_up.y = (Ay.y*fp.y + By.y*fp.x > Cy.y);

    fx.z = (Ao.z*fp.y + Bo.z*fp.x > Co.z);
    fx_left.z = (Ax.z*fp.y + Bx.z*fp.x > Cx.z);
    fx_up.z = (Ay.z*fp.y + By.z*fp.x > Cy.z);

    fx.w = (Ao.w*fp.y + Bo.w*fp.x > Co.w);
    fx_left.w = (Ax.w*fp.y + Bx.w*fp.x > Cx.w);
    fx_up.w = (Ay.w*fp.y + By.w*fp.x > Cy.w);

    //ir_lv1.x = ((e.x != f.x) && (e.x != h.x));
    //ir_lv1.y = ((e.y != f.y) && (e.y != h.y));
    //ir_lv1.z = ((e.z != f.z) && (e.z != h.z));
    //ir_lv1.w = ((e.w != f.w) && (e.w != h.w));
    ir_lv1 = ((e != f) && (e != h));

    //ir_lv2_left.x = ((e.x != g.x) && (d.x != g.x));
    //ir_lv2_left.y = ((e.y != g.y) && (d.y != g.y));
    //ir_lv2_left.z = ((e.z != g.z) && (d.z != g.z));
    //ir_lv2_left.w = ((e.w != g.w) && (d.w != g.w));
    ir_lv2_left = ((e != g) && (d != g));

    //ir_lv2_up.x = ((e.x != c.x) && (b.x != c.x));
    //ir_lv2_up.y = ((e.y != c.y) && (b.y != c.y));
    //ir_lv2_up.z = ((e.z != c.z) && (b.z != c.z));
    //ir_lv2_up.w = ((e.w != c.w) && (b.w != c.w));
    ir_lv2_up = ((e != c) && (b != c));

    float4 w1 = weighted_distance(e, c, g, i, h5, f4, h, f);
    float4 w2 = weighted_distance(h, d, i5, f, i4, b, e, i);

    // begin optimization: reduction of 6 instruction slots
    float4 df_fg = df(f, g);
    float4 df_hc = df(h, c);
    // end optimization

    float4 t1 = (coef * df_fg);
    float4 t2 = df_hc;
    float4 t3 = df_fg;
    float4 t4 = (coef * df_hc);

    //edr = bool4((w1.x < w2.x) && ir_lv1.x, 
    //            (w1.y < w2.y) && ir_lv1.y, 
    //            (w1.z < w2.z) && ir_lv1.z, 
    //            (w1.w < w2.w) && ir_lv1.w);
    edr = (w1 < w2) && ir_lv1;

    //edr_left = bool4((t1.x <= t2.x) && ir_lv2_left.x, 
    //                 (t1.y <= t2.y) && ir_lv2_left.y, 
    //                 (t1.z <= t2.z) && ir_lv2_left.z, 
    //                 (t1.w <= t2.w) && ir_lv2_left.w);
    edr_left = (t1 <= t2) && ir_lv2_left;

    //edr_up = bool4((t4.x <= t3.x) && ir_lv2_up.x, 
    //               (t4.y <= t3.y) && ir_lv2_up.y, 
    //               (t4.z <= t3.z) && ir_lv2_up.z, 
    //               (t4.w <= t3.w) && ir_lv2_up.w);
    edr_up = (t4 <= t3) && ir_lv2_up;

    //nc.x = (edr.x && (fx.x || edr_left.x && fx_left.x || edr_up.x && fx_up.x));
    //nc.y = (edr.y && (fx.y || edr_left.y && fx_left.y || edr_up.y && fx_up.y));
    //nc.z = (edr.z && (fx.z || edr_left.z && fx_left.z || edr_up.z && fx_up.z));
    //nc.w = (edr.w && (fx.w || edr_left.w && fx_left.w || edr_up.w && fx_up.w));
    nc = (edr && (fx || edr_left && fx_left || edr_up && fx_up));

    // to actually compile this shader, uncomment the following line
    // which reduces the instruction count to under 512
    //nc.zw = (float2)0;

    t1 = df(e, f);
    t2 = df(e, h);

    //px = bool4(t1.x <= t2.x, 
    //           t1.y <= t2.y, 
    //           t1.z <= t2.z, 
    //           t1.w <= t2.w);
    px = t1 <= t2;

    float3 res = nc.x ? px.x ? F : H : nc.y ? px.y ? B : F : nc.z ? px.z ? D : B : nc.w ? px.w ? H : D : E;

    return float4(res.x, res.y, res.z, 1.0);
}

technique mainTech
{
    pass mainPass
    {
        VertexShader = compile vs_3_0 main_vertex();
        PixelShader = compile ps_3_0 main_fragment();
    }
}

Foto-foto

Gambar asli oleh Redshrike telah ditingkatkan dengan faktor 4.

  • Pengambilan sampel titik

sebelum

  • xBR

setelah


Saya sudah menggunakan optimasi itu dalam jawaban saya. Itu adalah bagaimana saya bisa melewati kesalahan slot instruksi yang saya lihat.
test

Lupakan saja. Saya agak terlalu lambat :)
zogi

ir_lv1 = ((e != f) && (e != h)); ir_lv2_left = ((e != g) && (d != g)); ir_lv2_up = ((e != c) && (b != c)); Itu adalah optimasi yang baik yang saya lewatkan yang Anda temukan, saya akhirnya tidak membutuhkannya untuk masalah saya karena saya dapat mengurangi jumlah instruksi dengan optimasi lainnya.
test

Baik. Topik yang bagus. Saya belum pernah mendengar tentang algoritme ini sebelum pertanyaan Anda. Saya telah menemukan posting blog ini tentang hqx, yang membantu saya memahami algoritma. Saya sangat merekomendasikannya, jika Anda tertarik.
zogi

6

Saya berhasil ini. Itu tidak menggunakan filter hqx, ia menggunakan filter xBR (yang saya sukai). Bagi saya, ini bukan masalah. Jika Anda memerlukan filter hqx maka Anda ingin mengonversi file .cg menjadi setara XNA yang sesuai.

Untuk alasan kelengkapan dan pencarian, saya akan mengedit pertanyaan agar lebih ringkas dan kemudian memposting semua informasi yang relevan untuk menjawab pertanyaan di sini.


Langkah 1: Pengaturan Kode Game

Pertama, Anda kemungkinan besar ingin menyiapkan target render tempat Anda menggambar game Anda dalam skala 1: 1 dan kemudian membuat filter.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace xbr
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        RenderTarget2D renderTarget;
        Effect xbrEffect;
        Matrix projection;
        Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0);
        Texture2D pretend240x160Scene;

        // the bounds of your 1:1 scene
        Rectangle renderBounds = new Rectangle(0, 0, 240, 160);

        // the bounds of your output scene (same w:h ratio)
        Rectangle outputBounds = new Rectangle(0, 0, 720, 480);

        public Game1()
        {
           base.Content.RootDirectory = "Content";

           this.graphics = new GraphicsDeviceManager(this);
           this.graphics.PreferredBackBufferWidth = outputBounds.Width;
           this.graphics.PreferredBackBufferHeight = outputBounds.Height;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            this.spriteBatch = new SpriteBatch(base.GraphicsDevice);
            this.xbrEffect = Content.Load<Effect>("xbr");

            // a fake scene that is a 240x160 image
            this.pretend240x160Scene = base.Content.Load<Texture2D>("240x160Scene");
            this.renderTarget = new RenderTarget2D(base.GraphicsDevice, this.renderBounds.Width, this.renderBounds.Height);

            // default vertex matrix for the vertex method
            this.projection = Matrix.CreateOrthographicOffCenter(0, this.outputBounds.Width, this.outputBounds.Height, 0, 0, 1);

            // set the values of this effect, should only have to do this once
            this.xbrEffect.Parameters["matrixTransform"].SetValue(halfPixelOffset * projection);
            this.xbrEffect.Parameters["textureSize"].SetValue(new float[] { renderBounds.Width, renderBounds.Height });
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            base.GraphicsDevice.Clear(Color.CornflowerBlue);
            base.GraphicsDevice.SetRenderTarget(this.renderTarget);

            // draw your scene here scaled 1:1. for now I'll just draw
            // my fake 240x160 texture
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, 
                              SamplerState.PointClamp, null, null);

            spriteBatch.Draw(this.pretend240x160Scene, this.renderBounds, this.renderBounds, Color.White);

            spriteBatch.End();

            // now we'll draw to the back buffer
            base.GraphicsDevice.SetRenderTarget(null);

            // this renders the effect
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, 
                              SamplerState.PointClamp, null, null, this.xbrEffect);

            spriteBatch.Draw(this.renderTarget, this.outputBounds, this.renderBounds, Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}

Langkah 2: File Efek

Berikut ini adalah file efek yang kompatibel dengan XNA untuk menjalankan filter xBR.

// all identified optimizations have been amalgamated into this file
float2 textureSize;
float4x4 matrixTransform;

const static float coef = 2.0;
const static float3 yuv_weighted = float3(14.352, 28.176, 5.472);

sampler decal : register(s0);

float4 df(float4 A, float4 B)
{
    return abs(A - B);
}

float4 weighted_distance(float4 a, float4 b, float4 c, float4 d, 
                         float4 e, float4 f, float4 g, float4 h)
{
    return (df(a, b) + df(a, c) + df(d, e) + df(d, f) + 4.0 * df(g, h));
}

float4 main_vertex(inout float4 col0 : COLOR0, inout float2 tex0 : TEXCOORD0, 
                   inout float4 pos0 : POSITION0) : TEXCOORD1
{
    float2 ps = 1.0 / textureSize;

    pos0 = mul(pos0, matrixTransform);

    return float4(ps.x, 0, 0, ps.y);
}

float4 main_fragment(float4 pos0 : POSITION0, float2 tex0 : TEXCOORD0, 
                     float4 tex1 : TEXCOORD1) : COLOR0
{
    bool4 edr, edr_left, edr_up, px; // px = pixel, edr = edge detection rule
    bool4 ir_lv1, ir_lv2_left, ir_lv2_up;
    bool4 nc; // new_color
    bool4 fx, fx_left, fx_up; // inequations of straight lines.

    float2 fp = frac(tex0 * textureSize);
    float2 dx = tex1.xy;
    float2 dy = tex1.zw;

    float3 A  = tex2D(decal, tex0 - dx - dy).xyz;
    float3 B  = tex2D(decal, tex0 - dy).xyz;
    float3 C  = tex2D(decal, tex0 + dx - dy).xyz;
    float3 D  = tex2D(decal, tex0 - dx).xyz;
    float3 E  = tex2D(decal, tex0).xyz;
    float3 F  = tex2D(decal, tex0 + dx).xyz;
    float3 G  = tex2D(decal, tex0 - dx + dy).xyz;
    float3 H  = tex2D(decal, tex0 + dy).xyz;
    float3 I  = tex2D(decal, tex0 + dx + dy).xyz;
    float3 A1 = tex2D(decal, tex0 - dx - 2.0 * dy).xyz;
    float3 C1 = tex2D(decal, tex0 + dx - 2.0 * dy).xyz;
    float3 A0 = tex2D(decal, tex0 - 2.0 * dx - dy).xyz;
    float3 G0 = tex2D(decal, tex0 - 2.0 * dx + dy).xyz;
    float3 C4 = tex2D(decal, tex0 + 2.0 * dx - dy).xyz;
    float3 I4 = tex2D(decal, tex0 + 2.0 * dx + dy).xyz;
    float3 G5 = tex2D(decal, tex0 - dx + 2.0 * dy).xyz;
    float3 I5 = tex2D(decal, tex0 + dx + 2.0 * dy).xyz;
    float3 B1 = tex2D(decal, tex0 - 2.0 * dy).xyz;
    float3 D0 = tex2D(decal, tex0 - 2.0 * dx).xyz;
    float3 H5 = tex2D(decal, tex0 + 2.0 * dy).xyz;
    float3 F4 = tex2D(decal, tex0 + 2.0 * dx).xyz;

    float4 b = mul(float4x3(B, D, H, F), yuv_weighted);
    float4 c = mul(float4x3(C, A, G, I), yuv_weighted);
    float4 e = mul(float4x3(E, E, E, E), yuv_weighted);
    float4 d = b.yzwx;
    float4 f = b.wxyz;
    float4 g = c.zwxy;
    float4 h = b.zwxy;
    float4 i = c.wxyz;

    float4 i4 = mul(float4x3(I4, C1, A0, G5), yuv_weighted);
    float4 i5 = mul(float4x3(I5, C4, A1, G0), yuv_weighted);
    float4 h5 = mul(float4x3(H5, F4, B1, D0), yuv_weighted);
    float4 f4 = h5.yzwx;

    float4 Ao = float4(1.0, -1.0, -1.0, 1.0);
    float4 Bo = float4(1.0, 1.0, -1.0, -1.0);
    float4 Co = float4(1.5, 0.5, -0.5, 0.5);
    float4 Ax = float4(1.0, -1.0, -1.0, 1.0);
    float4 Bx = float4(0.5, 2.0, -0.5, -2.0);
    float4 Cx = float4(1.0, 1.0, -0.5, 0.0);
    float4 Ay = float4(1.0, -1.0, -1.0, 1.0);
    float4 By = float4(2.0, 0.5, -2.0, -0.5);
    float4 Cy = float4(2.0, 0.0, -1.0, 0.5);

    // These inequations define the line below which interpolation occurs.
    fx.x = (Ao.x * fp.y + Bo.x * fp.x > Co.x);
    fx.y = (Ao.y * fp.y + Bo.y * fp.x > Co.y);
    fx.z = (Ao.z * fp.y + Bo.z * fp.x > Co.z);
    fx.w = (Ao.w * fp.y + Bo.w * fp.x > Co.w);

    fx_left.x = (Ax.x * fp.y + Bx.x * fp.x > Cx.x);
    fx_left.y = (Ax.y * fp.y + Bx.y * fp.x > Cx.y);
    fx_left.z = (Ax.z * fp.y + Bx.z * fp.x > Cx.z);
    fx_left.w = (Ax.w * fp.y + Bx.w * fp.x > Cx.w);

    fx_up.x = (Ay.x * fp.y + By.x * fp.x > Cy.x);
    fx_up.y = (Ay.y * fp.y + By.y * fp.x > Cy.y);
    fx_up.z = (Ay.z * fp.y + By.z * fp.x > Cy.z);
    fx_up.w = (Ay.w * fp.y + By.w * fp.x > Cy.w);

    ir_lv1      = ((e != f) && (e != h));
    ir_lv2_left = ((e != g) && (d != g));
    ir_lv2_up   = ((e != c) && (b != c));

    float4 w1 = weighted_distance(e, c, g, i, h5, f4, h, f);
    float4 w2 = weighted_distance(h, d, i5, f, i4, b, e, i);
    float4 df_fg = df(f, g);
    float4 df_hc = df(h, c);
    float4 t1 = (coef * df_fg);
    float4 t2 = df_hc;
    float4 t3 = df_fg;
    float4 t4 = (coef * df_hc);

    edr      = (w1 < w2)  && ir_lv1;
    edr_left = (t1 <= t2) && ir_lv2_left;
    edr_up   = (t4 <= t3) && ir_lv2_up;

    nc = (edr && (fx || edr_left && fx_left || edr_up && fx_up));

    t1 = df(e, f);
    t2 = df(e, h);
    px = t1 <= t2;

    float3 res = nc.x ? px.x ? F : H : 
                 nc.y ? px.y ? B : F : 
                 nc.z ? px.z ? D : B : 
                 nc.w ? px.w ? H : D : E;

    return float4(res.xyz, 1.0);
}

technique T0
{
    pass P0
    {
        VertexShader = compile vs_3_0 main_vertex();
        PixelShader = compile ps_3_0 main_fragment();
    }
}

Hasil

Tekstur yang saya gunakan untuk membuat 240x160:

Input xBR

Output dari menjalankan game:

Output xBR

Sumber

File .cg yang saya konversi menjadi XNA kompatibel datang dari sini . Jadi kredit pergi ke mereka untuk menulisnya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.