2

I’m trying to create a simple ASCII 3D spinning cube animation in the Windows console using C++:

#include <iostream>
#include <cmath>
#include <cstring>
#include <windows.h>
#include <thread>
using namespace std;

const int WIDTH = 80;
const int HEIGHT = 40;
const float cubeWidth = 20;

float A = 0, B = 0, C = 0;
float distanceFromCamera = 100;
float K1 = 40;

char buffer[WIDTH * HEIGHT];
float zBuffer[WIDTH * HEIGHT];

float cosA, sinA, cosB, sinB, cosC, sinC; // biến lượng giác dùng chung

void gotoxy(HANDLE hConsole, int x, int y) {
    COORD pos{(SHORT)x, (SHORT)y};
    SetConsoleCursorPosition(hConsole, pos);
}

void rotate(float i, float j, float k, float &x, float &y, float &z) {
    // rotation quanh 3 trục
    float x1 = i;
    float y1 = j * cosA - k * sinA;
    float z1 = j * sinA + k * cosA;

    float x2 = x1 * cosB + z1 * sinB;
    float y2 = y1;
    float z2 = -x1 * sinB + z1 * cosB;

    x = x2 * cosC - y2 * sinC;
    y = x2 * sinC + y2 * cosC;
    z = z2;
}

void calculatePoint(float i, float j, float k, char ch) {
    float x, y, z;
    rotate(i, j, k, x, y, z);
    z += distanceFromCamera;

    float ooz = 1 / z;
    int xp = int(WIDTH / 2 + K1 * ooz * x * 2);
    int yp = int(HEIGHT / 2 - K1 * ooz * y);

    int idx = xp + yp * WIDTH;
    if (idx >= 0 && idx < WIDTH * HEIGHT) {
        if (ooz > zBuffer[idx]) {
            zBuffer[idx] = ooz;
            buffer[idx] = ch;
        }
    }
}

int main() {
    HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

    // Ẩn con trỏ nhấp nháy
    CONSOLE_CURSOR_INFO cursorInfo;
    GetConsoleCursorInfo(hConsole, &cursorInfo);
    cursorInfo.bVisible = FALSE;
    SetConsoleCursorInfo(hConsole, &cursorInfo);

    while (true) {
        fill(buffer, buffer + WIDTH * HEIGHT, ' ');
        fill(zBuffer, zBuffer + WIDTH * HEIGHT, 0);

        // tính lượng giác 1 lần m���i frame
        cosA = cosf(A); sinA = sinf(A);
        cosB = cosf(B); sinB = sinf(B);
        cosC = cosf(C); sinC = sinf(C);

        for (float i = -cubeWidth; i < cubeWidth; i += 0.5f) {
            for (float j = -cubeWidth; j < cubeWidth; j += 0.5f) {
                calculatePoint(i, j, -cubeWidth, '@');
                calculatePoint(cubeWidth, j, i, '$');
                calculatePoint(-cubeWidth, j, -i, '~');
                calculatePoint(-i, j, cubeWidth, '#');
                calculatePoint(i, -cubeWidth, -j, ';');
                calculatePoint(i, cubeWidth, j, '+');
            }
        }

        gotoxy(hConsole, 0, 0);

        for (int k = 0; k < WIDTH * HEIGHT; k++) {
            cout << ((k % WIDTH) ? buffer[k] : '\n');
        }

        A += 0.04f;
        B += 0.03f;

        this_thread::sleep_for(chrono::milliseconds(16));
    }

    return 0;
}

The rendering works fine, but there’s one major issue: Each frame prints below the previous one, so the terminal keeps scrolling down endlessly instead of updating in place like a normal animation.

Even though I use "\x1b[H" to move the cursor back to the top, the console still scrolls downward — every new frame is printed below the previous one.

How can I correctly render each frame in the same position (without scrolling) on Windows console, preferably without using third-party graphics libraries? (Im using VSCode on Windows 11).

What I’ve tried:

  • Using "\x1b[2J" or "\033[H" to clear screen / move cursor
  • Using std::flush after printing
  • Running on different terminals (CMD, PowerShell, Windows Terminal)

Expected behavior: I want the cube to rotate in place (in a fixed area of the console), not scroll down.

7
  • You are using cout - this means standard output. This is wrong tool for animation in text mode. You should use some API this is able to output text directly into console. Commented Oct 6 at 9:53
  • Anyway you should do cout << std::flush or cout.flush() before sleep_for. This could be related. Commented Oct 6 at 9:56
  • 1
    Sidenote: for (int k = 0; k < WIDTH * HEIGHT; k++) { cout << ((k % WIDTH) ? buffer[k] : '\n'); } is a rather slow way to print the whole buffer. Just add another character to the width and make that a \n and then print the whole buffer at once. Commented Oct 6 at 11:51
  • 3
    Do not try to mix high level output (std::cout) and low level action (SetConsoleCursorPosition). You cannot know exactly how C++ cout is implemented (it is an implementation detail that can change without notice) and relying on expected interaction is a sure path to problems. Commented Oct 6 at 11:55
  • A third-party TUI library provides a uniform API for most terminals. You write your code using the API and the library translates it for the specific terminal. If you don't want to use a library, you have to write code for each terminal you want to support. Commented Oct 6 at 12:14

2 Answers 2

3

One way to do this, since you are on Windows, is to use WriteConsoleOutput to just paint an entire frame buffer to the console in one call. The following for example will paint an asterisk traveling in a circle:

#include <windows.h>
#include <cmath>

constexpr int k_columns = 80, k_rows = 40;
CHAR_INFO fb[k_columns * k_rows];

int main() {
    HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);

    COORD bufSize{ k_columns, k_rows };
    COORD bufCoord{ 0, 0 };
    SMALL_RECT rect{ 0, 0, k_columns - 1, k_rows - 1 };

    double theta = 0.0;
    double radius = 15.0;
    double orig_x = 40.0;
    double orig_y = 20.0;

    for (int i = 0; i < k_columns * k_rows; ++i) {
        fb[i].Char.AsciiChar = ' ';   
        fb[i].Attributes = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE;
    }
    int x = 0, y = 0;

    while (true) {

        fb[y * k_columns + x].Char.AsciiChar = ' ';
        
        x = static_cast<int>(std::round(radius * std::cos(theta) + orig_x));
        y = static_cast<int>(std::round(radius * std::sin(theta) + orig_y));

        fb[y * k_columns + x].Char.AsciiChar = '*';

        WriteConsoleOutputA(h, fb, bufSize, bufCoord, &rect); 
        Sleep(16);

        theta += 3.14159 / 180.0;
    }
}

WriteConsoleOutput is deprecated however. You are now supposed to use "console virtual terminal sequences"...

Sign up to request clarification or add additional context in comments.

1 Comment

WriteConsoleOutput is deprecated because it's not cross-platform. Since we're writing especially and only for Windows then deprecated warning may be safetly suspended.
0

You're writing a Windows Console app so you don't have to bother about portability and standard streams at all. Use whole winapi, there's no good reason to limit yourself.

First: GetStdHandle(STD_OUTPUT_HANDLE) may or may not return a handle to console. Open a new handle instead:

HANDLE hConsole = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);

...

// since it's created, don't forget to close the handle later
CloseHandle(hConsole);

Second: use functions listed here: https://learn.microsoft.com/en-us/windows/console/console-functions

Especially use https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput

BOOL WINAPI WriteConsoleOutput(
  _In_          HANDLE      hConsoleOutput,
  _In_    const CHAR_INFO   *lpBuffer,
  _In_          COORD       dwBufferSize,
  _In_          COORD       dwBufferCoord,
  _Inout_       PSMALL_RECT lpWriteRegion
);

This function allows you to place any character in any place on the console, with setting any foreground and background color for it. And it doesn't matter where the console cursor currently is. The effect of this method is immediate, unlike typing into cout. There will be no screen flicker.

1 Comment

"any foreground and background color" - As long as you stick to the 16 supported colors. "True color" support is only supplied via virtual terminal sequences, not the console API.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.