DECK
DECK
🏅 AI 마스터
⚔️ 베테랑 파트너

ChatGPT 슈퍼볼 광고 따라잡기

어제는 북미 스포츠계 최고의 이벤트인 NFL의 결승전 슈퍼볼이 열렸습니다.

슈퍼볼은 그 명성 만큼이나 하프타임쇼, 경기 중간중간 광고에 대한 관심이 매우 높은데요.

특히 광고의 경우 30초당 단가가 100억원을 훌쩍 넘는, 세계에서 가장 비싼 광고 슬롯인데요.

openAI에서는 무려 60초의 광고 시간을 구입하여 'The Intelligence Age'라는 제목으로 챗GPT의 광고를 내보냈습니다.

이번 광고는 지난 주 나왔던 openAI의 디자인 방향성을 정리한 Refresh 영상처럼 2D 모션그래픽 위주로 제작 되었는데,

가장 두드러진 특징은 도트 위주로 된 그래픽 아트가 사용되었다는 점이죠.

보드를 들고있는 남자의 픽셀 화 된 이미지

수렵 시대를 나타내는 동굴 벽화 느낌의 도트 그래픽부터

흰색 실루엣이있는 검은 배경
흰색 배경에 검은 색과 흰색 점선 원

불과 바퀴의 발견

말과 라이더의 흑백 픽셀 화 된 이미지
옥수수 벡터 | 가격 1 크레딧 USD $ 1

유목과 농경

하늘에있는 배의 흑백 이미지

대항해시대 등

다양한 영상을 도트 그래픽으로 처리 했습니다.

일부는 모션 그래픽 베이스로 제작 된 걸로 보이며, 디테일한 영상은 소라를 통해 제작,

프로그래밍 적으로 변환한걸로 보였습니다.

그래서 뭐...?

어 음 그래서요. 만들었습니다. 무엇을요? 동영상을 선택하면 이런 도트 그래픽으로 만들어주는 코드를요. ChatGPT에게 코딩을 시켜서 일단 작동되는 코드를 받고, 테스트 돌려보는데 1분, 버튼 위치 등 미세 조정하는데는 거의 시간이 걸리지 않았구요.

한국 웹 사이트의 스크린 샷

처음에는 유튜브 주소를 주면 되게 만들어달라고 했는데 생각해보니 문제가 있겠더라구요

일단 지피티가 그렇게 만들어주지도 않았구요.

먼저 p5.js가 무엇인가 설명을 하자면, 프로세싱이라고 하는 자바 기반의 미디어아트, 제너러티브아트, 크리에이티브 코딩에 사용되는 언어와 IDE를 자바스크립트 기반으로 옮긴 프로젝트입니다.

명암을 기반으로 도트의 크기를 다르게 해달라는 것은 그렇게 해야 화면에서 밝은 영역이 흰색이 더 도드라져보이면서 영상의 형체를 구분하는데 더 유리하고, 결과물의 퀄리티를 높일 수 있기 때문입니다.

그 외에 해상도 자동 설정 기능과 저장 기능을 추가해둔 코드입니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <title>동영상 업로드 및 도트 효과 - 자동 녹화</title>
  <!-- p5.js 라이브러리 로드 -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script>
  <style>
    body { margin: 20px; }
  </style>
</head>
<body>
  <!-- HTML에 미리 생성해 둔 녹화 시작/중지 버튼 (수동으로 중지할 수 있음) -->
  <button id="recordBtn" disabled>저장 시작</button>
  <script>
    let video;
    let fileInput;
    let isVideoLoaded = false;
    let cnv;            // p5.js 캔버스
    let recordBtn;      // 녹화 버튼 (p5.Element)
    let mediaRecorder;
    let recordedChunks = [];
    let stream;
    let recording = false;
    let statusDiv;      // 녹화 진행 상태를 보여줄 DOM 요소

    function setup() {
      // 캔버스 생성 (미리보기가 표시됨)
      cnv = createCanvas(640, 480);
      pixelDensity(1);
      cnv.position(20, 80);

      // HTML에 있는 녹화 버튼을 p5의 select()로 참조하고 위치 설정
      recordBtn = select("#recordBtn");
      recordBtn.position(cnv.position().x, cnv.position().y + height + 10);
      recordBtn.mousePressed(toggleRecording);

      // 동영상 파일 업로드를 위한 파일 입력 요소 생성 및 위치 지정
      fileInput = createFileInput(handleFile);
      fileInput.position(cnv.position().x, recordBtn.position().y + recordBtn.elt.offsetHeight + 10);

      // "진행중" 상태를 보여줄 DOM 요소 생성 (캔버스 위쪽, 캔버스 영역 밖에 배치)
      statusDiv = createDiv("");
      statusDiv.style("font-size", "32px");
      statusDiv.style("color", "red");
      statusDiv.position(cnv.position().x, cnv.position().y - 40);
    }

    // 파일 업로드 시 호출되는 함수
    function handleFile(file) {
      if (file.type === "video") {
        // 기존 동영상이 있다면 제거
        if (video) {
          video.remove();
        }
        // 업로드된 파일로 동영상 생성
        video = createVideo([file.data], videoLoaded);
        video.hide(); // 원본 video 엘리먼트는 숨김 (처리된 결과는 캔버스에 표시)
        isVideoLoaded = true;
      } else {
        console.log("동영상 파일을 업로드해주세요.");
      }
    }

    // 동영상이 로드되면 호출되는 함수
    function videoLoaded() {
      // 동영상의 원본 해상도(내부 video 태그의 videoWidth, videoHeight)를 사용하여 캔버스 크기 재조정
      const vidWidth = video.elt.videoWidth;
      const vidHeight = video.elt.videoHeight;
      resizeCanvas(vidWidth, vidHeight);
      // 캔버스와 DOM 요소들의 위치를 동영상 해상도에 맞게 재조정
      cnv.position(20, 80);
      recordBtn.position(cnv.position().x, cnv.position().y + height + 10);
      fileInput.position(cnv.position().x, recordBtn.position().y + recordBtn.elt.offsetHeight + 10);
      statusDiv.position(cnv.position().x, cnv.position().y - 40);

      console.log("동영상 해상도:", vidWidth, vidHeight);

      // 동영상 재생 설정 (음소거, 한 번 재생)
      video.volume(0);
      video.play();

      // 캔버스의 captureStream()을 이용해 MediaStream 획득 (실시간 캡처)
      stream = cnv.elt.captureStream();

      // 저장할 파일 포맷 지정 (브라우저에 따라 mp4가 안될 경우 webm 사용)
      let options = { mimeType: "video/mp4" };
      if (!MediaRecorder.isTypeSupported(options.mimeType)) {
        console.log("video/mp4가 지원되지 않으므로 video/webm으로 저장합니다.");
        options = { mimeType: "video/webm" };
      }

      try {
        mediaRecorder = new MediaRecorder(stream, options);
      } catch (e) {
        console.error("MediaRecorder 생성 오류:", e);
        return;
      }

      mediaRecorder.ondataavailable = handleDataAvailable;
      mediaRecorder.onstop = handleStop;

      // 동영상이 로드되었으므로 녹화 시작 버튼 활성화
      recordBtn.removeAttribute("disabled");

      // 동영상 로드 후 자동으로 녹화를 시작합니다.
      startRecording();
    }

    // 녹화 데이터가 준비될 때마다 호출 (녹화 데이터 저장)
    function handleDataAvailable(event) {
      if (event.data.size > 0) {
        recordedChunks.push(event.data);
      }
    }

    // 녹화 중지 후 호출되어 파일 다운로드 실행
    function handleStop(event) {
      const blob = new Blob(recordedChunks, { type: mediaRecorder.mimeType });
      const url = URL.createObjectURL(blob);
      // 다운로드 링크 생성 후 자동 클릭하여 파일 저장
      const a = document.createElement("a");
      a.style.display = "none";
      a.href = url;
      a.download = mediaRecorder.mimeType === "video/mp4" ? "output.mp4" : "output.webm";
      document.body.appendChild(a);
      a.click();
      // 다운로드 후 메모리 해제
      setTimeout(() => {
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);
      }, 100);
      // 녹화 종료 시 진행 상태 메시지 제거
      statusDiv.html("");
    }

    // 녹화 시작/중지 버튼 클릭 시 동작 전환
    function toggleRecording() {
      if (!recording) {
        startRecording();
      } else {
        stopRecording();
      }
    }

    function startRecording() {
      recordedChunks = [];
      mediaRecorder.start();
      recording = true;
      recordBtn.html("저장 중지");
      console.log("녹화 시작");
      // 녹화 시작 시 진행 상태 메시지를 표시 (캔버스 밖 DOM 요소에 업데이트)
      statusDiv.html("진행중");
    }

    function stopRecording() {
      if (mediaRecorder && recording) {
        mediaRecorder.stop();
        recording = false;
        recordBtn.html("저장 시작");
        console.log("녹화 중지");
        // 녹화 종료 시 진행 상태 메시지 제거
        statusDiv.html("");
      }
    }

    // draw() 함수에서 동영상에 도트 효과를 적용하여 캔버스에 그립니다.
    function draw() {
      background(0);
      if (isVideoLoaded && video.loadedmetadata) {
        video.loadPixels();
        if (video.pixels.length > 0) {
          const stepSize = 10; // 샘플링 간격
          for (let y = 0; y < video.height; y += stepSize) {
            for (let x = 0; x < video.width; x += stepSize) {
              const index = (y * video.width + x) * 4;
              const r = video.pixels[index];
              const g = video.pixels[index + 1];
              const b = video.pixels[index + 2];
              const brightnessVal = (r + g + b) / 3;
              // 어두운 부분(밝기 0)은 점의 크기가 0, 밝은 부분은 stepSize 크기의 점이 되도록 매핑
              const dotSize = map(brightnessVal, 0, 255, 0, stepSize);
              noStroke();
              fill(255);
              ellipse(x, y, dotSize, dotSize);
            }
          }
        }
      }

      // 동영상 재생이 끝나면 자동으로 녹화를 중지합니다.
      if (isVideoLoaded && video.time() >= video.duration() && recording) {
        stopRecording();
      }
    }
  </script>
</body>
</html>

그 외에 버튼 위치 등을 수정해서 처음 완성한 코드는 위와 같습니다

이 코드를 codepen이나 code sandbox 같은 툴에 올려서 바로 테스트해보실 수 있구요

동영상 업로드 및 도트 효과 - 자동 녹화

여기에서도 테스트 해보실 수 있습니다.

해당 코드를 거친 결과물은 이렇게 나오게 됩니다.

이런식의 미술적 작업을 코딩을 통해 하는 것을

크리에이티브 코딩이라고 해서 미디어아트의 하위 분류로 보는데요.

요즘 계속 이런걸 하고 있어서... 많은 관심 부탁드립니다.

4
1개의 답글

👉 이 게시글도 읽어보세요