[Godot] Line2D 선 그리기

Godot 2024. 2. 21. 18:02 |
반응형

간단한 선을 그려보자.

 

Line2D 노드 두 개와 스크립트를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using Godot;
using System.Collections.Generic;
 
public partial class Control : Node2D
{
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        DrawCircle();
        DrawPentagram();
    }
 
    // 원 그리기
    public async void DrawCircle()
    {
        Line2D line = GetNode<Line2D>("Line2D");
        //line.Closed = true;
        // Closed를 여기서 변경하면 원이 그려지는 방식이 달라진다.
        line.Width = 2;
        line.DefaultColor = Colors.Red;
        line.Position = new Vector2(200200);
 
        float radius = 100;
        float angle;
 
        for (int i = 0; i < 360; i++)
        {
            angle = Mathf.DegToRad(i);
            line.AddPoint(CalcCirclePoint(radius, angle));
            await ToSignal(GetTree().CreateTimer(0.01), SceneTreeTimer.SignalName.Timeout);
        }
 
        line.Closed = true;
    }
 
    // 원 포인트 계산
    public Vector2 CalcCirclePoint(float radius, float angle)
    {
        float x = Mathf.Cos(angle);
        float y = Mathf.Sin(angle);
 
        return new Vector2(x * radius, y * radius);
    }
 
    // 별 그리기
    public async void DrawPentagram()
    {
        Line2D line = GetNode<Line2D>("Line2D2");
        line.Width = 2;
        line.DefaultColor = Colors.Blue;
        line.Position = new Vector2(200200);
 
        List<Vector2> points = new List<Vector2>();
        points.Add(new Vector2(6080));
        points.Add(new Vector2(0-100));
        points.Add(new Vector2(-6080));
        points.Add(new Vector2(95-25));
        points.Add(new Vector2(-95-25));
        //points.Add(new Vector2(60, 80));
        // 마지막에 Closed 프로퍼티에 true를 대입하기 때문에 별의 시작점을 다시 추가할
        // 필요는 없다.
 
        for (int i = 0; i < points.Count; i++)
        {
            line.AddPoint(points[i]);
            await ToSignal(GetTree().CreateTimer(1), SceneTreeTimer.SignalName.Timeout);
        }
 
        line.Closed = true;
    }
}
 

 

위와 같이 코드를 작성한다.

 

 

※ 참고

Line2D

Custom drawing in 2D

 

반응형
Posted by J-sean
:
반응형

GIMP를 이용해 투명 배경을 만들어 보자.

 

Mario.png
0.00MB

투명 배경이 필요한 그림을 준비한다.

 

GIMP로 불러온다.

 

Layer - Transparency - Color to Alpha... 를 선택한다.

 

Color의 컬러 픽커를 이용하면 정확히 한 픽셀을 선택하기가 어렵다. 위 그림과 같이 여러 픽셀이 선택되면 평균값이 계산 되는거 같다.

 

Color를 선택해 원하는 색을 직접 입력하자. [0..100], [0..255] 선택에 주의한다.

 

투명하게 만들 색을 입력하고 확인하면 Alpha Channel이 생성되고 배경이 투명하게 바뀐다.

 

File - Export As... 를 선택하고 PNG 파일로 저장한다.

 

반응형
Posted by J-sean
:
반응형

Godot Sprite에는 Colorkey를 설정하는 옵션이 없다.PNG 이미지는 알파 채널을 통해 투명 배경을 설정할 수 있어 상관 없지만 JPG, BMP같은 이미지 파일은 투명 배경이 없어 곤란하다. 하지만 셰이더를 이용해 같은 효과를 만들 수 있다.

 

스프라이트를 추가하고 텍스쳐를 지정한다.

 

Material - New Shader Material을 선택한다.

 

Shader - New Shader를 선택한다.

 

적당한 이름을 지정하고 셰이더 파일을 생성한다.

 

 

Shader Editor를 활성화 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
shader_type canvas_item;
// Get texture and a key that we want as transparent
uniform sampler2D Diffuse;
uniform vec4 ChromaKey : source_color;
 
void vertex() {
    // Called for every vertex the material is visible on.
}
 
void fragment() {
    // Called for every pixel the material is visible on.
    
    // The color value is the sampled pixel color
    COLOR = texture(Diffuse, UV);
 
    // If the color is the same as the chroma key, make it transparent.
    if (COLOR == ChromaKey) {
        COLOR.a = 0.0;
    }
}
 
//void light() {
    // Called for every pixel for every light affecting the CanvasItem.
    // Uncomment to replace the default light processing function with this one.
//}
 

 

위와 같은 코드를 작성한다.

 

Shader Parameters를 아래와 같이 지정한다.

ChromaKey: R:54 G:61 B:82 A:255 (Godot 이미지의 배경색)

Diffuse: 텍스쳐 이미지와 동일하게 지정

※ Diffuse에 이미지 지정시 Texture 파라미터의 filter를 선택할 수 없어 이미지가 흐리게 보일 수 있다. 필요하다면 셰이더 코드에서 Diffuse 선언을 아래와 같이 수정한다.

uniform sampler2D Diffuse : filter_nearest;

 

Godot 이미지의 배경이 투명해졌다.

 

 

하지만 게임을 실행해 보면 배경이 투명하지 않다.

 

1
2
3
4
// If the color is the same as the chroma key, make it transparent.
if (distance(COLOR, ChromaKey) < 0.001) {
    COLOR.a = 0.0;
}
 

 

vec4 타입의 ChromaKey와 COLOR 데이터 타입의 비교에서 약간의 오차가 발생하는거 같다.

if()을 위와 같이 수정한다.

 

원하는 색이 정상적으로 투명하게 변한다.

 

※ 참고

이렇게 하나의 스프라이트에 셰이더를 적용하면 같은 씬의 다른 스프라이트에도 모두 같은 방식으로 셰이더를 적용해야 아래와 같은 에러가 생기지 않는 경우가 발생할 수 있다.

E 0:00:01:0199   swap_buffers: Vulkan: Cannot submit graphics queue. Error code: VK_ERROR_DEVICE_LOST
  <C++ Error>    Condition "err" is true. Returning: ERR_CANT_CREATE
  <C++ Source>   drivers/vulkan/vulkan_context.cpp:2536 @ swap_buffers()
E 0:00:01:0208   prepare_buffers: Vulkan: Did not create swapchain successfully. Error code: VK_NOT_READY
  <C++ Error>    Condition "err != VK_SUCCESS" is true. Breaking.
  <C++ Source>   drivers/vulkan/vulkan_context.cpp:2459 @ prepare_buffers()

 

반응형
Posted by J-sean
:
반응형

투명한 배경의 윈도우를 만들어 보자.

 

스프라이트를 추가하고 스크립트를 연결한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Godot;
 
public partial class Control : Sprite2D
{
    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        GetViewport().TransparentBg = true;
    }
 
    public override void _Process(double delta)
    {
        RotationDegrees += 180.0f * (float)delta;
    }
}
 

 

위와 같은 코드를 작성한다.

 

Project Settings - Display - Window - Borderless / Transparent 옵션을 모두 체크한다.

 

실행하면 배경은 물론 타이틀바도 없는 윈도우에서 게임이 플레이된다.

 

반응형
Posted by J-sean
:
반응형

AnimatedSprite나 AnimationPlayer를 사용하지 않고 Sprite만으로도 간단한 애니메이션을 만들 수 있다.

 

Sprite2D를 추가하고 Texture를 지정한다. Hframes는 8, Vframes는 7로 조절한다.

 

Scale을 적절히 조절하고 스크립트도 추가한다.

 

char_blue.png
0.02MB

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Godot;
 
public partial class Control : Sprite2D
{
    public override async void _Process(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
        {
            // 점프 애니메이션
            for (int i = 32; i < 40; i++)
            {
                await ToSignal(GetTree().CreateTimer(0.2),
                    SceneTreeTimer.SignalName.Timeout);
                Frame = i;
            }
        }
 
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            // 전체 애니메이션
            for (int v = 0; v < Vframes; v++)
                for (int h = 0; h < Hframes; h++)
                {
                    await ToSignal(GetTree().CreateTimer(0.2),
                        SceneTreeTimer.SignalName.Timeout);
                    FrameCoords = new Vector2I(h, v);
                }
        }
    }
}
 

 

위와 같은 코드를 작성한다.

엔터키를 누르면 점프, ESC키를 누르면 전체 애니메이션이 플레이 된다. 애니메이션이 플레이 되는 동안 키를 여러번 동시에 누르면 애니메이션이 겹친다.

 

 

반응형
Posted by J-sean
:
반응형

애니메이션 플레이어를 사용해 보자.

 

씬에 스프라이트를 추가하고 텍스쳐를 지정한다.

 

애니메이션 플레이어를 추가한다.

 

애니메이션 탭에서 Animation 버튼을 클릭하고 New를 선택한다.

 

애니메이션 이름(ani_1)을 지정한다.

 

Add Track - Property Track을 선택한다.

 

Sprite2D를 선택한다.

 

position을 선택한다.

 

애니메이션 탭 - position 트랙에서 마우스 오른쪽 클릭 - Insert Key 선택

 

키가 만들어지면 0초로 옮기고, 스프라이트를 이동해서 키를 하나 더 만들어 1초로 옮긴다.

 

스프라이트에 스크립트를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Godot;
 
public partial class Control : Sprite2D
{
    public AnimationPlayer Player;
 
    public override void _Ready()
    {
        Player = GetNode<AnimationPlayer>("../AnimationPlayer");
    }
 
    public override void _Process(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            Player.Play("ani_1");
    }
}
 

 

추가된 스크립트에 위와 같은 코드를 작성한다.

 

게임을 실행하고 엔터키를 누르면 애니메이션이 플레이된다.

 

※ 참고

Animation

 

반응형
Posted by J-sean
:
반응형

캐릭터에 이동에 관성을 적용해 보자.

 

1
2
3
4
5
6
7
8
9
10
11
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left""ui_right""ui_up""ui_down");
if (direction != Vector2.Zero)
{
    velocity.X = direction.X * Speed;
}
else
{
    velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed);
}
 

 

CharacterBody2D를 상속한 스크립트의 _PhysicsProcess()에는 위와 같은 키 입력 처리 코드가 있다.

 

1
2
3
4
5
6
7
8
9
10
11
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left""ui_right""ui_up""ui_down");
if (direction != Vector2.Zero)
{
    velocity.X = direction.X * Speed;
}
else
{
    velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed * (float)delta);
}
 

 

위와 같이 수정한다.

 

방향키를 떼는 순간 한번에 정지하지 않고 관성이 적용된다.

 

하지만 위 코드는 빠르게 방향을 바꾸는 경우엔 관성이 적용되지 않는다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
// Get the input direction and handle the movement/deceleration.
// As good practice, you should replace UI actions with custom gameplay actions.
Vector2 direction = Input.GetVector("ui_left""ui_right""ui_up""ui_down");
if (direction != Vector2.Zero)
{
    //velocity.X = direction.X * Speed;
    velocity.X = Mathf.MoveToward(Velocity.X, direction.X * Speed, Speed * (float)delta);
}
else
{
    velocity.X = Mathf.MoveToward(Velocity.X, 0, Speed * (float)delta);
}
 

 

위와 같이 수정한다.

좀 더 빠른(느린) 방향 전환이 필요하다면 MoveToward()의 Speed * (float)delta에 적당한 수치를 곱한다.

 

빠르게 방향을 바꾸어도 관성이 적용되어 슈퍼 마리오 같은 움직임을 보인다.

 

※ 참고

Using CharacterBody2D/3D

 

반응형
Posted by J-sean
:
반응형

 

2023.10.18 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 2. 구현

 

위 링크를 참고해 만들어진 물에 돌을 떨어뜨려 보자.

 

Stone씬을 만든다.

Stone(RigidBody2D) - Script(stone.cs)를 추가한다.

Sprite2D - Texture를 추가한다.

CollisionShape2D - Shape을 추가한다.

 

1
2
3
4
5
6
7
8
9
10
using Godot;
 
public partial class stone : RigidBody2D
{
    public override async void _Process(double delta)
    {
        await ToSignal(GetTree().CreateTimer(2.0), SceneTreeTimer.SignalName.Timeout);
        QueueFree();
    }
}
 

 

생성되고 2초 후 삭제되는 스크립트(stone.cs)를 작성한다.

 

Main씬에 스크립트(Main.cs)를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Godot;
 
public partial class Main : Node2D
{
    public PackedScene Scene;
 
    public override void _Ready()
    {
        Scene = ResourceLoader.Load<PackedScene>("res://stone.tscn");
    }
 
    public override void _Input(InputEvent @event)
    {
        if (@event is InputEventMouseButton button)
            if (button.Pressed && button.ButtonIndex == MouseButton.Left)
            {
                RigidBody2D stone = Scene.Instantiate<RigidBody2D>();
                stone.Position = GetViewport().GetMousePosition();
                AddChild(stone);
            }
    }
}
 

 

왼쪽 마우스 버튼을 클릭하면 돌을 생성하는 스크립트(Main.cs)를 작성한다.

 

 

마우스를 클릭하면 돌이 생성되고 아래로 떨어진다.

 

spring씬에 Area2D와 CollisionShape2D를 추가한다.

CollisionShape2D - RectangleShape2D를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
 
    public CollisionShape2D collision;
 
    public void Initialize(int x_position)
    {
        Position = new Vector2(x_position, 100);
        height = Position.Y;
        target_height = Position.Y;
        velocity = 0;
 
        collision = GetNode<CollisionShape2D>("Area2D/CollisionShape2D");
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        float x = height - target_height;
        float resist = -dampening * velocity;
        force = -spring_constant * x + resist;
 
        velocity += force;
        Position += new Vector2(0, velocity);
    }
 
    public void set_collision_width(int distance)
    {
        ((RectangleShape2D)collision.Shape).Size = new Vector2(distance,
            collision.Shape.GetRect().Size.Y);
    }
}
 

 

spring.cs를 위와 같이 수정한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public override void _Ready()
{
    k = 0.015f;
    d = 0.05f;
    spread = 0.0002f;
    passes = 8;
    depth = 1000;
    bottom = (int)GlobalPosition.Y + depth;
    water_polygon = GetNode<Polygon2D>("Polygon2D");
 
    springs = new List<spring>();
    spring_number = 10;
    distance_between_springs = 100;
    scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
    for (int i = 0; i < spring_number; i++)
    {
        Node2D s = scene.Instantiate<Node2D>();
        AddChild(s);
        springs.Add(s as spring);
 
        int x_position = distance_between_springs * i + 100;
        (s as spring).Initialize(x_position);
        (s as spring).set_collision_width(distance_between_springs);
    }
 
    wave_position = new Curve2D();
}
 

 

WaterBody.cs는 set_collision_width() 호출 코드만 추가하면 된다.

 

 

실행 시 Collision Shape이 보이도록 설정한다.

 

Collision Shape이 생성되고 표시된다.

 

Spring - Area2D - body_entered 시그널을 Spring 스크립트와 연결한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
 
    public CollisionShape2D collision;
 
    public int index;
    public float stone_velocity_factor;
    [Signal]
    public delegate void SplashEventHandler(int index, float velocity);
    // index, stone_velocity_factor, Splash 시그널 추가
 
    public void Initialize(int x_position, int id)
    {
        Position = new Vector2(x_position, 100);
        height = Position.Y;
        target_height = Position.Y;
        velocity = 0;
 
        index = id;
        stone_velocity_factor = 0.02f;
        // index, stone_velocity_factor 초기화
 
        collision = GetNode<CollisionShape2D>("Area2D/CollisionShape2D");
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        float x = height - target_height;
        float resist = -dampening * velocity;
        force = -spring_constant * x + resist;
 
        velocity += force;
        Position += new Vector2(0, velocity);
    }
 
    public void set_collision_width(int distance)
    {
        ((RectangleShape2D)collision.Shape).Size = new Vector2(distance,
            collision.Shape.GetRect().Size.Y);
    }
 
    public void OnArea2dBodyEntered(Node2D body)
    {
        float speed = (body as RigidBody2D).LinearVelocity.Y * stone_velocity_factor;
        // We need to disable the spring's collision so that we don't trigger the body enter
        // signal more than once.
        // Must be deferred as we can't change physics properties on a physics callback.        
        body.SetDeferred(CollisionShape2D.PropertyName.Disabled, true);
        // Signal
        EmitSignal(SignalName.Splash, index, speed);
    }
}
 

 

spring.cs 를 위와 같이 수정한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    public int passes;
    public List<spring> springs;
    public int spring_number;
    public int distance_between_springs;
    public int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
    public Curve2D wave_position;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        bottom = (int)GlobalPosition.Y + depth;
        water_polygon = GetNode<Polygon2D>("Polygon2D");
 
        springs = new List<spring>();
        spring_number = 10;
        distance_between_springs = 100;
        scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
        for (int i = 0; i < spring_number; i++)
        {
            Node2D s = scene.Instantiate<Node2D>();
            AddChild(s);
            springs.Add(s as spring);
 
            int x_position = distance_between_springs * i + 100;
            (s as spring).Initialize(x_position, i);
            // Index 추가
            (s as spring).set_collision_width(distance_between_springs);
            (s as spring).Splash += splash;
            // Signal 추가
        }
 
        wave_position = new Curve2D();
    }
 
    public override void _PhysicsProcess(double delta)
    {
        //if (Input.IsActionJustPressed("ui_accept"))
        //    splash(5, 20);
        // 테스트용 splash() 호출 삭제
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        List<float> right_deltas = new List<float>();
 
        for (int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i - 1].height);
                    springs[i - 1].velocity += left_deltas[i];
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i + 1].height);
                    springs[i + 1].velocity += right_deltas[i];
                }
            }
        }
 
        draw_wave();
        draw_waterbody();
    }
 
    public void splash(int index, float speed)
    {
        if (index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        Vector2[] baked_points = wave_position.GetBakedPoints();
        for (int i = 0; i < baked_points.Length; i++)
        {
            surface_points.Add(baked_points[i]);
        }
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
 
        water_polygon.Polygon = surface_points.ToArray();
    }
 
    public override void _Draw()
    {
        DrawPolyline(wave_position.Tessellate(), Colors.Blue, 5);
    }
 
    public void draw_wave()
    {
        wave_position.ClearPoints();
 
        Vector2 control0 = new Vector2(-500);
        Vector2 control1 = new Vector2(500);
 
        foreach (spring item in springs)
        {
            wave_position.AddPoint(item.Position, control0, control1);
        }
 
        QueueRedraw();
    }
}
 

 

WaterBody.cs를 위와 같이 수정한다.

 

마우스를 클릭하면 돌이 떨어지고 물결이 자연스럽게 파동친다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Node2D collided_with = null;
 
public void OnArea2dBodyEntered(Node2D body)
{
    float speed = (body as RigidBody2D).LinearVelocity.Y * stone_velocity_factor;
 
    // We need to disable the spring's collision so that we don't trigger the body enter
    // signal more than once.
    // Must be deferred as we can't change physics properties on a physics callback.
    //body.SetDeferred(CollisionShape2D.PropertyName.Disabled, true);
    if (body == collided_with)
        return;
    else
        collided_with = body;
 
    // Signal
    EmitSignal(SignalName.Splash, index, speed);
}
 

 

spring.cs의 OnArea2dBodyEntered()를 위와 같이 바꾸면 SetDeferred()를 사용할 때 보다 훨씬 반응이 빠르고 자연스러운 파동이 만들어진다.

 

 

물의 높이는 spring.cs - Initialize() - Position 변수에서 간단히 변경할 수 있다.

 

Water.zip
10.27MB

전체 프로젝트 파일

 

※ 참고

Make a Splash With Dynamic 2D Water Effects

 

반응형
Posted by J-sean
: