• 로그인
  • 장바구니에 상품이 없습니다.

ASCII 아스키 렌더링





이건 Rogue라는 게임의 스크린샷인데 옛날에 우리 조상님들은 이런 식으로 게임을 만들었습니다.

문자로 모든 것을 그려놨는데 왜 문자를 이용해서 그려놨냐면

그때 당시에는 PC를 켜봤자 GUI가 아니라 문자만 입출력할 수 있는 터미널창이 주로 나왔기 때문에

거기서 게임 그래픽을 표현하려면 ASCII라고 부르던 90여개의 문자를 가지고 그림을 그려야 했습니다.


원래 컴퓨터는 0과 1로 이루어진 이진수 밖에 모릅니다.

근데 특정 이진수를 문자로 표현하기 위한 ASCII라는 규칙을 누가 만들었는데 그걸 이용하는 소프트웨어들은 위와 같은 문자들 표현이 가능했습니다.

그래서 그 문자들로 그림을 그린 것들을 아스키 아트라고 부릅니다.

Braille ASCII라는 문자들을 쓰면 훨씬 디테일 좋은 그림도 가능했습니다.

문자에 색상을 쓰기 시작한 이후로는 ANSI art라는 것도 등장했습니다.

네모난 문자에 색을 입혀서 그린 것인데

인터넷 초창기 게시판 서비스 같은 곳에서 유행을 탔습니다.

요즘도 간혹 그래픽이 필요하지만 성능이 제한되어있는 곳에서 쓰기도 하고 (PDF Doom)

터미널에서 뭔가 그림을 보여주고 싶을 때 사용하기도 합니다.

그래서 해커같고 멋있어보이니까 우리도 이미지를 입력하면 ASCII 아트로 렌더링시켜주는 엔진 같은걸 만들어보도록 합시다.

이미지에 있는 명암을 ASCII로 표현해주는 정도로 만들어볼건데 그건 되게 쉽습니다.

직접 만들어보면 응용도 재밌게 할 수 있으니까요.




원리 

코딩할 때는 코드부터 짜는게 아니라 작동방식부터 한글로 설명하고 그걸 코드로 번역만 하면 쉽습니다. 

이런건 어떤 식으로 만들었냐면 잘 살펴보면 되게 쉬운데  

이미지에서 어두운 부분은 큰 글자로 표현하고

밝은 부분은 작은 글자로 표현해놨습니다.

그게 끝임

그래서 이미지에 있는 하나하나의 픽셀을 꺼내본 다음에 밝기에 따라서 문자로 하나하나 치환을 해버리면 될 것 같군요.

그럼 우선 이미지를 그냥 흑백으로 변환부터 하면 될 것 같습니다.

왜냐면 흑백으로 변환해버리면 어두운 부분이랑 밝은 부분 구분이 쉬워지니까 편리하잖아요.

흑백으로 바꾸는건 어떻게 하는 거냐면 이미지는 확대해보면 픽셀들이 여러개 붙어있고 그 픽셀마다 RGB 색상값이 있습니다.

밝기 == 0.299R + 0.587G + 0.114B

그걸 특정 공식에 넣으면 밝기 수치를 구할 수 있습니다.

숫자는 맘대로 조절해도 됩니다.

그럼 밝기 값이 0~255로 나올텐데 예를 들어 15가 나왔으면

RGB(15, 15, 15) 이런 색으로 기존 픽셀의 색상을 바꾸면 흑백으로 이미지가 변합니다.



근데 이미지를 흑백으로 실제로 바꾸려고 하는게 아니라

밝기수치가 0부터 20정도인 픽셀은 .

밝기수치가 20부터 40정도인 픽셀은 -

밝기수치가 40부터 60정도인 픽셀은 +

이런 문자로 변환하면 되겠습니다.

근데 모든 픽셀에 대해서 할 필요가 없고 듬성듬성 샘플을 뽑아서 글자로 변환해도 될 것 같습니다.

한 5픽셀마다 샘플을 뽑아서 해버리면 화질은 떨어지겠지만 성능은 더 낫지 않을까요.

아무튼 이걸 그대로 코드로 옮기기만 하면 되겠습니다.




코드

index.html 파일과 이미지를 같은 폴더에 준비합시다.

전 이걸 준비해봤습니다.

그 다음에 이미지에 있는 픽셀을 하나하나 출력하려면

자바스크립트로 하려면 이미지를 우선 canvas에 로드해보면 가능합니다.

<canvas id="canvas" width="300" height="300"></canvas>

<script>
const img = new Image();
img.src = "doro.png";
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
img.addEventListener("load", () => {
ctx.drawImage(img, 0, 0);
});
</script>

저는 doro.png라는 이미지를 canvas에 로드하라고 해봤습니다.

그럼 ctx.getImageData() 어쩌구라는걸 쓸 수 있는데 그 안에 4개 파라미터를 입력하면 특정 픽셀에 있는 rgba 색상값을 출력할 수 있습니다.

ctx.getImageData(x좌표, y좌표, x크기, y크기) 

어디있는 픽셀을 출력할건지 x좌표, y좌표 입력하고 (가장 왼쪽 위가 0,0)

x크기, y크기는 출력할 영역의 가로세로 크기 입력하면 됩니다.

예를 들어 ctx.getImageData(0, 0, 1, 1) 이러면 가장 왼쪽 위부터 가로세로 1x1 사이즈의 픽셀들의 rgba 값을 전부 출력해줍니다.

<canvas id="canvas" width="300" height="300"></canvas>

<script>
const img = new Image()
img.src = "doro.png"
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
img.addEventListener("load", () => {
ctx.drawImage(img, 0, 0)
let pixel = ctx.getImageData(0, 0, 1, 1).data
console.log(pixel)
});
</script>

뒤에 .data 찍고 그러면 출력할 수 있습니다.

한번 출력해보면 [255, 255, 255, 255] 이런게 나옵니다.

왜냐면 제가 준비한 이미지는 가장 왼쪽 위에 있는 픽셀 1개는 아마 흰색이라서 그런 것 같군요.




문자로 바꾸기

그럼 픽셀에 있던 색상값의 밝기를 구해보도록 합시다.


let gray = 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2] 

아까 만든 공식을 쓰면 픽셀에 있던 색상의 밝기를 구할 수 있습니다. 

근데 우리는 이걸 문자로 변환을 해야하지 않겠습니까.

@$#Y!=+~- 이런 식으로 문자를 한 10개 정도 준비하고

밝기가 0부터 24는 @

밝기가 25부터 49는 $

이런 식으로 바꿔보도록 합시다.



<canvas id="canvas" width="300" height="300"></canvas>

<h4></h4>
<script>
const img = new Image()
img.src = "doro.png"
const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
img.addEventListener("load", () => {
ctx.drawImage(img, 0, 0)
let pixel = ctx.getImageData(0, 0, 1, 1).data

let ascii = '@$#Y!=+~- '
let gray = 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2]
let index = Math.floor(gray/25.6)
let result = ascii[index]
document.querySelector('h4').innerText += result

});
</script>

밝기 값에 나눗셈 좀 하면 0부터 9까지 숫자가 나올거라

0이 나오면 0번째 문자 꺼내고

1이 나오면 1번째 문자 꺼내고

그걸 위에 있는 <h4> 태그에 집어넣으라고 했습니다.

이러면 가장 왼쪽 위의 픽셀 1개만 문자로 바꿨을 뿐인데 가로줄 전체에 있는 픽셀에 대해서 똑같은 작업을 시켜주려면 어떻게 합니까.

코드를 복붙하거나 반복문 쓰면 되겠습니다.



for (let i = 0; i < 300; i++){
let pixel = ctx.getImageData(i, 0, 1, 1).data;
let ascii = '@$#Y!=+~- '
let gray = 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2];
let index = Math.floor(gray/25.6)
let result = ascii[index]
document.querySelector('h4').innerText += result
}

이미지 가로사이즈가 300px이라 그만큼 반복하라고 해봤습니다.

근데 그럼 맨 윗줄의 픽셀만 전부 변환해줄텐데 그 다음줄도 변환하려면 이 코드를 세로줄 갯수만큼 복붙해야겠군요.



for (let j = 0; j < 300; j+=5){

for (let i = 0; i < 300; i+=5){
let pixel = ctx.getImageData(i, j, 1, 1).data;
let ascii = '@$#Y!=+~- '
let gray = 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2];
let index = Math.floor(gray/25.6)
let result = ascii[index]
document.querySelector('h4').innerText += result
if (i > 290){
document.querySelector('h4').innerHTML += '<br>'
}
}

}

복붙이 귀찮으면 반복문이 있습니다.

실은 처음부터 이중 반복문부터 돌려도 되겠지만 그럼 초보들은 이해가 어렵지 않을까요.



진짜 되나 미리보기 띄워보면 이런 식으로 나오는군요.

뇌가 찌그러졌는데 문자다보니까 약간 폰트 스타일을 조절 해보면 이쁘게 렌더링해볼 수있습니다.


body { 
margin: 0;
font-family: 'consolas';
line-height: 85%;
letter-spacing: 2px;
font-size: 10px;
}

이런 CSS 스타일을 줘봤습니다.

consolas가 코딩할 때 자주쓰는 폰트라 그런가 문자마다 폭이 매우 비슷합니다.


개선사항

1. 큰 이미지를 변환해보면 로딩이 느린데

이게 픽셀이 많은데 하나하나 읽어들여서 그런 것 같군요. 

아까 그 getImageData에서 1개의 픽셀만 읽는게 아니라 이미지 전체를 한 번에 읽으면 훨씬 빨라질 것 같은데 한 번 응용해봅시다.

2. 반복문을 300회 돌리는 이유가 이미지 사이즈가 300x300px 이기 때문입니다.

근데 그럼 가로 400px의 이미지를 첨부하면 반복문을 400번 돌려야하는데 그거 매번 수정하기 귀찮으면 개선해봅시다.

3. 한글도 재밌습니다.


4. 동영상도 프레임마다 픽셀데이터를 추출할 수 있어서 동영상도 아스키 아트로 변환해서 재생이 가능합니다.

5. 픽셀을 5개마다 추출해서 문자로 변환중인데 그 간격을 조절하면 화질도 다양하게 변경 가능하지 않을까요.

이거저거 재밌는 짓이 가능하니 한 번 만들어보도록 합시다.





2025년 10월 25일

About

현재 월 700명 신규수강중입니다.

  (09:00~20:00) 빠른 상담은 카톡 플러스친구 코딩애플 (링크)
  admin@codingapple.com
  이용약관, 개인정보처리방침
ⓒ Codingapple, 강의 예제, 영상 복제 금지
top

© Codingapple, All rights reserved. 슈퍼로켓 에듀케이션 / 서울특별시 강동구 고덕로 19길 30 / 사업자등록번호 : 212-26-14752 온라인 교육학원업 / 통신판매업신고번호 : 제 2017-서울강동-0002 호 / 개인정보관리자 : 박종흠