用控制台输出个球

本文介绍了如何在B站视频的启发下,使用C++编程在控制台输出3D图形,特别是画一个球体,并添加简单的光影效果。通过向量运算和数学公式,实现了球体表面亮度的计算。此外,还展示了如何利用双缓冲技术动态更新光源位置,使图形产生运动感。
摘要由CSDN通过智能技术生成

诞生想法

在刷B站的时候看到了用控制台输出3D图形的视频,非常感兴趣,由于我很逊,所以我只会输出个球。
这个教程可是小学生都能懂呢!


实现

首先我们先来画一个球(前提:球心在原点,半径为20)

for(int i = 1; i < 40; ++i){
    for (int j = 1; j < 80; ++j){
      if(hypot(i - 20, j * 1.0 / 2.0 - 20) < 20) {
        vec nod(j * 1.0 / 2.0 - 20.0, - sqrt(20 * 20 - (i - 20) * (i - 20) - (j * 1.0 / 2.0 - 20.0) * (j * 1.0 / 2.0 - 20.0)), 20 - i);
        putchar('#');
      }
      else putchar(' '); //这里要打空格,不然会出现神奇的图形,别问我为什么
    }
    putchar('\n');
  }

然后画出来大概是这样的
一个圆
然后我们来添加光影。
通过参考一些教程,我大概知道物体表面亮度怎么算,然后就简化了一下式子: N i × L i N_i \times L_i Ni×Li,这里 N i N_i Ni是指表面单位法向量, L i L_i Li是指光源方向单位向量。
接着我们先写一个向量结构体:

struct vec{
  double x, y, z;
  vec(double x = 0, double y = 0, double z = 0): x(x), y(y), z(z) {}
  vec operator+ (const vec& n) const{ //向量加减法
    return {x + n.x, y + n.y, z + n.z};
  }
  vec operator- (const vec& n) const{
    return {x - n.x, y - n.y, z - n.z};
  }
  double operator* (const vec& n) const{ //向量内积
    return x * n.x + y * n.y + z * n.z;
  }
  vec& changelen(double len) { //改变向量长度
    double prelen = sqrt(x * x + y * y + z * z);//现在的长度
    x *= len / prelen;
    y *= len / prelen;
    z *= len / prelen;
    return *this;
  }
}l; //光源坐标

然后顺便定义一个不同亮度对应的字符数组,从暗到亮:

char s[19] = ".:'`-!;^?~*)(%&$#"; //当然你定义更多也可以

然后再根据那个式子简单地写出更新代码就好啦!(其实最开始没有放在函数里,这个函数后面有用)

int getlight(vec nod, vec law) { //点坐标和法向量
  int ans = floor(law.changelen(3) * (l - nod).changelen(3));//将向量的模都定义为3,因为总共17个字符,这样好算
  if (ans < -8) ans = -8; //简单操作一下,防止意外
  else if (ans > 8) ans = 8;
  return ans + 8;
}
void update(){
  for(int i = 1; i < 40; ++i){
    for (int j = 1; j < 80; ++j){
      if(hypot(i - 20, j * 1.0 / 2.0 - 20) < 20) {
      	//获得这个点地坐标,记得是20-i,不然方向会反(还是不要问为什么)
        vec nod(j * 1.0 / 2.0 - 20.0, - sqrt(20 * 20 - (i - 20) * (i - 20) - (j * 1.0 / 2.0 - 20.0) * (j * 1.0 / 2.0 - 20.0)), 20 - i);
        putchar(s[getlight(nod, nod)]); //因为中心就在(0,0),所以法向量等于点的坐标
      }
      else putchar(' ');
    }
    putchar('\n');
  }
}

然后输出试试看 (这里光源是在 ( − 20 , − 55 , 100 ) (-20,-55,100) (20,55,100))
一个相信光的球这里分界处有点小问题,相信聪明的同学们一定能找到解决办法的!

让光源动起来

但是这只是一张图片,我们试图让它动起来,然后在加一点小细节。
你行你让球动起来吧,反正我只会让光源动起来
把光标隐藏起来算细节吗,你个细狗
我们要让一个点在空间内一个圆上动,首先我们确定圆心为 ( x 1 , y 1 , z 1 ) (x_1,y_1,z_1) (x1,y1,z1),然后单位法向量为 ( x 2 , y 2 , z 2 ) (x_2,y_2,z_2) (x2,y2,z2) (默认点旋转为法向量朝上时的逆时针方向),半径为 r r r
我们可以先假设光源 l l l z = 0 z=0 z=0和以 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)为圆心的圆上,这种情况下 l l l的坐标就很好表示,可以将 l l l表示为, ( r cos ⁡ θ , r sin ⁡ θ , 0 ) (r\cos{\theta},r\sin{\theta},0) (rcosθ,rsinθ,0),此时单位法向量为 ( 0 , 0 , 1 ) (0,0,1) (0,0,1),我们先不考虑圆心的变化,法向量的改变根据我们小学二年级学过的罗德里格斯公式可以知道这个变化能用矩阵表示出来,是
A = [ y 2 2 + x 2 2 z 2 x 2 2 + y 2 2 x 2 y 2 ( z 2 − 1 ) x 2 2 + y 2 2 x 2 x 2 y 2 ( z 2 − 1 ) x 2 2 + y 2 2 x 2 2 + y 2 2 z 2 x 2 2 + y 2 2 y 2 − x 2 − y 2 z 2 ] A=\begin{bmatrix} \frac{y_2^2+x_2^2z_2}{x_2^2+y_2^2}&\frac{x_2y_2(z_2-1)}{x_2^2+y_2^2}&x_2\\ \frac{x_2y_2(z_2-1)}{x_2^2+y_2^2}&\frac{x_2^2+y_2^2z_2}{x_2^2+y_2^2}&y_2\\ -x_2&-y_2&z_2 \end{bmatrix} A= x22+y22y22+x22z2x22+y22x2y2(z21)x2x22+y22x2y2(z21)x22+y22x22+y22z2y2x2y2z2
然后我们就可以得到变换后 l l l的位置为 A ⋅ [ r cos ⁡ θ r sin ⁡ θ 0 ] A\cdot\begin{bmatrix}r\cos{\theta}\\r\sin{\theta}\\0\end{bmatrix} A rcosθrsinθ0
最后再加上圆心就能得到最终的位置了。
注意:当法向量垂直于水平面时,矩阵应变为
[ z 2 0 x 2 0 z 2 y 2 − x 2 − y 2 z 2 ] \begin{bmatrix} z_2&0&x_2\\ 0&z_2&y_2\\ -x_2&-y_2&z_2 \end{bmatrix} z20x20z2y2x2y2z2
然后开始写代码,刷新当然是使用高逼格的双缓冲啦!
完整代码:

#include<bits/stdc++.h> //作者可真是逊欸用这么拉的头文件
#include<windows.h>
#include<cwchar>
using namespace std;
char s[19] = ".:'`-!;^?~*)(%&$#"; //有心人士可以改一改,目前分界条纹较为明显
const double Pi = std::acos(-1);
const double eps = 1e-10;
HANDLE hOutBuf1, hOutBuf2; //标准缓存区和另一个缓存区
COORD coord = {0, 0}; //指针位置
char data[2000]; //写入缓冲区的数据
int cnt; //目前写入的个数(主要不想推公式了)
DWORD bytes; //实际写入数据大小,可以判断是否所有数据都写入缓冲区了,没啥卵用
void init() { //初始化,双缓冲
  hOutBuf1 = CreateConsoleScreenBuffer(
    GENERIC_WRITE,
    FILE_SHARE_WRITE,
    NULL,
    CONSOLE_TEXTMODE_BUFFER,
    NULL
  );
  hOutBuf2 = CreateConsoleScreenBuffer(
    GENERIC_WRITE,
    FILE_SHARE_WRITE,
    NULL,
    CONSOLE_TEXTMODE_BUFFER,
    NULL
  );
  SetConsoleActiveScreenBuffer(hOutBuf2); //显示2号缓存区
  CONSOLE_CURSOR_INFO cci;
  cci.bVisible = 0;
  cci.dwSize = 1;
  SetConsoleCursorInfo(hOutBuf1, &cci);//隐藏光标(小细节)
  SetConsoleCursorInfo(hOutBuf2, &cci);
}
void SwapBuf() { //交换缓冲
  HANDLE tmp;
  tmp = hOutBuf1;
  hOutBuf1 = hOutBuf2;
  hOutBuf2 = tmp;
}
struct vec{ //向量(坐标)类(结构体)
  double x, y, z;
  vec(double x = 0, double y = 0, double z = 0): x(x), y(y), z(z) {}
  vec operator+ (const vec& n) const{
    return {x + n.x, y + n.y, z + n.z};
  }
  vec operator- (const vec& n) const{
    return {x - n.x, y - n.y, z - n.z};
  }
  double operator* (const vec& n) const{
    return x * n.x + y * n.y + z * n.z;
  }
  vec& changelen(double len) {
    double prelen = sqrt(x * x + y * y + z * z);
    x *= len / prelen;
    y *= len / prelen;
    z *= len / prelen;
    return *this;
  }
}l(-20,-55,100);
int getlight(vec nod, vec law) { //获取这个位置的光照强度
  int ans = floor(law.changelen(3) * (l - nod).changelen(3));
  if (ans < -8) ans = -8;
  else if (ans > 8) ans = 8;
  return ans + 8;
}
void update(){ //更新
  for(int i = 1; i < 40; ++i){
    cnt = 0;
    for (int j = 1; j < 80; ++j){
      if(hypot(i - 20, j * 1.0 / 2.0 - 20) < 20) {
        vec nod(j * 1.0 / 2.0 - 20.0, - sqrt(20 * 20 - (i - 20) * (i - 20) - (j * 1.0 / 2.0 - 20.0) * (j * 1.0 / 2.0 - 20.0)), 20 - i);
        data[cnt++] = s[getlight(nod, nod)];
      }
      else data[cnt++] = ' ';
    }
    coord.Y = i - 1;
    WriteConsoleOutputCharacterA(hOutBuf1, data, cnt, coord, &bytes);
  }
  SwapBuf();
}
void circleupdate(vec o, vec law, double r) { //圆心,圆所在平面法向量,半径
  law.changelen(1); //防熊操作
  double adder = 1; //一次转多少度
  double a00, a01, a11;
  if (fabs(law.x) > eps && fabs(law.y) > eps) {
    a00 = (law.y * law.y + law.x * law.x * law.z) / (law.x * law.x + law.y * law.y); //就是算矩阵乘法,自己看去
    a01 = (law.x * law.y * (law.z - 1.0)) / (law.x * law.x + law.y * law.y);
    a11 = (law.x * law.x + law.y * law.y * law.z) / (law.x * law.x + law.y * law.y);
  }
  else{
    a00 = a11 = law.z;
    a01 = 0;
  }
  register double co, si;
  for (double angle = 0;; angle = angle + adder + ((angle + adder) > 360 ? -360 : 0)) {
    co = cos(angle * Pi / 180.0);
    si = sin(angle * Pi / 180.0);
    l.x = r * co * a00 + r * si * a01;
    l.y = r * co * a01 + r * si * a11;
    l.z = -r * co * law.x - r * si * law.y;
    l = l + o;
    update();
    Sleep(20); //刷新速率
  }
}
int main()
{
  init();
  circleupdate(vec(0, 0, 40), vec(0, 0, 1), 40); //可以随便填
  return 0;
}

成果展示

成果图 成果图

后记

其实我想添加一个自定义路径循环的(你问为什么不随机动?你不觉得这种操作很逊吗),然后用三次B样条曲线来平滑路径(为啥要平滑呢?第一是很有逼格;第二是我只会三次B样条曲线),而且这个东西看起来很简单也很逊,那个矩阵也是我用mathematica算的,而且还多次算错了,望大佬提出一些意见~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值