LiDAR Sensor

Introduction

Sensor 에는 Camera, Radar, Ultrasound, and Lidar 가 있다. Camera 같은 경우는 Lateral 의 정보들을 우리가 보는 View 에 들어오고, 그 View 에는 수많은 Pixel 정보들을 가지고 있다. Radar 같은 경우에는, Distance 의 정보들을 가지고 올수 있으며, 특히 Velocity 정보들을 가지고 올수 있기 때문에 driver assistance system 에 들어가는 “adaptive cruise control” 이나 “autonomous emergency braking” 등 사용이 간다. 일단 Radar 같은 경우 electromagnetic wave 를 쏴서, 어떤 물체에 부딫혔을때, run-time of the signal 을 받아서 distance 의 값을 가지고 올수 있으며, Dopller effect 로 인해서, 물체의 움직임의 frequency shift 를 활용해서, velocity 를 구할수 있다. Camera 와 달리 weather condition 에 영향을 받진 않지만 low spatial resolution 을 가지고 있다. 그 영향은 Metal 같은 Object 가 아닐경우 다 refracted 된다고 한다면, 그 signal 은 약한 return signal 이 기 때문이다. [참고: radar 는 24GHz radar sensor 가 있는데, 이건 wider 하며, Long Range radar sensor 같은 경우 77GHz 가 있다.]

그다음은 최신 기술인 Lidar Sensor 이다. Lidar 는 beams of laster light 을 쏴서, object 로 부터 bouncing 한 시간을 기록한다. 이렇게 보면 Radar 랑 비슷하지만, 일단 Lidar 는 360 degree arc 를 쏴서, 3D point maps 에 대한 정보를 per second 당 Measure 을 한다.

  1. 너무 비싸다른것(lowering the price per unit)
  2. Decreasing package size
  3. Increasing sensing range and resolution

등이 존재한다. 그래서 LiDAR 의 Alternative approach 는 non-scanning sensor(Flash Lidar) 를 사용하는건데, 여기서 “Flash” 라는 거는 FOV(Field of View) 에 Laser source 를 한번 다쏘는식이다. 마치 필름카메라가 사진을 찍을때처럼 빛을 한번 뽱 싸주는거고, 쏴서 reflected laser pulse 만 가지고 오면 된다. 하지만 FOV 가 정해져있으니, narrow field 와 limited range 를 들고 있다는점이 drawback 이다. 그래서 우리가 실제 보는건 Roof-mounted scanning Lidar 를 사용하고 4 쪽 사이드에 사용되는건 non-scanning lidar sensor 를 사용한다.

간단한 Sensor Criteria 를 인용된걸 써보려고 한다.

Range : LiDAR and radar systems can detect objects at distances ranging from a few meters to more than 200m. Many LiDAR systems have difficulties detecting objects at very close distances, whereas radar can detect objects from less than a meter, depending on the system type (either long, mid or short range) . Mono cameras are not able to reliably measure metric distance to object - this is only possible by making some assumptions about the nature of the world (e.g. planar road surface). Stereo cameras on the other hand can measure distance, but only up to a distance of approx. 80m with accuracy deteriorating significantly from there.
Spatial resolution : LiDAR scans have a spatial resolution in the order of 0.1° due to the short wavelength of the emitted IR laser light . This allows for high-resolution 3D scans and thus characterization of objects in a scene. Radar on the other hand can not resolve small features very well, especially as distances increase. The spatial resolution of camera systems is defined by the optics, by the pixel size on the image and by its signal-to-noise ratio. Details on small object are lost as soon as the light rays emanating from them are spread to several pixels on the image sensor (blurring). Also, when little ambient light exists to illuminate objects, spatial resolution decreases as objects details are superimposed by increasing noise levels of the image sensor.
Robustness in darkness : Both radar and LiDAR have an excellent robustness in darkness, as they are both active sensors. While daytime performance of LiDAR systems is very good, they have an even better performance at night because there is no ambient sunlight that might interfere with the detection of IR laser reflections. Cameras on the other hand have a very reduced detection capability at night, as they are passive sensors that rely on ambient light. Even though there have been advances in night time performance of image sensors, they have the lowest performance among the three sensor types.
Robustness in rain, snow, fog : One of the biggest benefits of radar sensors is their performance under adverse weather conditions. They are not significantly affected by snow, heavy rain or any other obstruction in the air such as fog or sand particles. As an optical system, LiDAR and camera are susceptible to adverse weather and its performance usually degrades significantly with increasing levels of adversity.
Classification of objects : Cameras excel at classifying objects such as vehicles, pedestrians, speed signs and many others. This is one of the prime advantages of camera systems and recent advances in AI emphasize this even stronger. LiDAR scans with their high-density 3D point clouds also allow for a certain level of classification, albeit with less object diversity than cameras. Radar systems do not allow for much object classification.
Perceiving 2D structures : Camera systems are the only sensor able to interpret two-dimensional information such as speed signs, lane markings or traffic lights, as they are able to measure both color and light intensity. This is the primary advantage of cameras over the other sensor types.
Measure speed : Radar can directly measure the velocity of objects by exploiting the Doppler frequency shift. This is one of the primary advantages of radar sensors. LiDAR can only approximate speed by using successive distance measurements, which makes it less accurate in this regard. Cameras, even though they are not able to measure distance, can measure time to collision by observing the displacement of objects on the image plane. This property will be used later in this course.
System cost : Radar systems have been widely used in the automotive industry in recent years with current systems being highly compact and affordable. The same holds for mono cameras, which have a price well below US$100 in most cases. Stereo cameras are more expensive due to the increased hardware cost and the significantly lower number of units in the market. LiDAR has gained popularity over the last years, especially in the automotive industry. Due to technological advances, its cost has dropped from more than US$75,000 to below US$5,000. Many experts predict that the cost of a LiDAR module might drop to less than US$500 over the next few years.
Package size : Both radar and mono cameras can be integrated very well into vehicles. Stereo cameras are in some cases bulky, which makes it harder to integrate them behind the windshield as they sometimes may restrict the driver's field of vision. LiDAR systems exist in various sizes. The 360° scanning LiDAR is typically mounted on top of the roof and is thus very well visible. The industry shift towards much smaller solid-state LiDAR systems will dramatically shrink the system size of LiDAR sensors in the very near future.
Computational requirements : LiDAR and radar require little back-end processing. While cameras are a cost-efficient and easily available sensor, they require significant processing to extract useful information from the images, which adds to the overall system cost.

Available Lidar Types

일단 Lidar Type 중에 Scanning LiDAR 중에서 Motorized Optomechanical Scanners 가 most common 한 LiDAR Type 중에 하나이다. Velodyne 에서 만들어졌으며, 64-beam rotating line scanner 이다.

이 LiDAR 의 장점을 List-up 해보자면, 아래와 같은 장점을 가ㅣㅈ고 있으며, 이런 LiDAR 을 가지고 있는 Type 은 transmitter-reciever channel이 존재하고 360 도의 FOV 를 가지고 있고, Receiver 와 Emitter 가 Vertically 하게 잘싸옇져있다.

  1. Long Ranging Distance
  2. Wide Horizontal FOV
  3. Fast Scanning Speed

물론 high-quality 의 Point-Cloud data 를 얻을 수 있는 반면, 이거에 따른 단점도 존재한다. 일단 High Power Consumption, Physical 한 충격에 대한 민감한 정도, 그리고 마지막으로 bulky 하기 때문에 high price 라는 단점을 가지고 있다.

다른 한종류로는 Non-Scanning Flash Lidar 가 있다. 일단 Non-Scanning 에서 알아볼수 있듯이, sequential reconstruction 을 할수 있는게 아니라, camera 처럼 flash 를 data 수집하는 원리이다. 어떤 Array 에서 광선이 나와서, 각 Element 들이 tof receive 를 하는 방식이다. 즉 이때에 각 Pixel 값들이 하나 나온다. 이 부분 같은경우는 2D 를 Rasterization 하는 기법과 비슷하다.

일단 vibration 에 robust 하지만 단점이라고 하면, 한번 Flash 했을때, 전체의 Scene 이 들어와야하므로 Beam 을 쏠때의 큰 Energy 지가 필요하다. 하지만, 이러한 Energy 를 줄이려고 한다면, Receive 할때의 SNR 도 고려해야한다.

그 이외에 Optical Phase Array (OPA) 및 MEMS Mirror-based Quasi Solid-State LiDAR 가 존재한다.

LiDAR

일단 Most Common Lidar Sensor 는 “pulsed Lidar” 이다. a laser source 로 부터 laser beam scene 으로 burst or emit 한 이후에, 어떤 물체에 부딫혔을때, 굴절되거나 반사를 통해서 LiDAR 의 receiver 로 도착한다. time of flight 을 구하기 위해선, range R (distance) 를 구할수 있는데, 바로 공식은 R = (1/2n) * c * (delta t). 여기서 c 는 speed of light 이고 n 은 eta 라고 부르기도 하며 1.0 이라고 가정한다.

typical lidar sensor 의 Pipeline 을 한번 봐보자.

Laser source 로 부터 burst 할수 있게끔 Amplifier 르 ㄹ 해준다. 이럴때 laser 의 pulse 는 picoseconds 나 nanoseconds 정도 generate 이 도니다. 그런다음 beam scanner 와 transmitter optics 의 도움을 받아 Target 에다가 쏜다. 그런다음에 어떤 물체에 부딫혔을때, scatter 된 pulse energy 가 receiver lens 에 도착한이후에 amplify 가 되고, voltage signal 로 변경한다.

아래의 그림은 time of flight 을 구하는 부분을 그래프로 표현한거다.

Lidar Equation

Lidar Range Map

아래의 그림을 보면 Lidar 데이터가 왼쪽에서는 앞 차량의 뒷부분이 보이고, 전혀 차선(Lane) 또는 Road Surface 들이 보이지 않는다.

이런 Lidar scan 을 보는 방법중에 하나가 봐로 Range Image 이다. 이 Range Image 의 Data Structure 은 image 처럼 이지만 Lidar sensor 에서 한번 돌린 이미지라고 볼수 있다. 아래의 그림을 한번 보자.

일단 row 의 정보는 elevation angle, pitch 에 대한 정보가 있고, column 정보에는 azimuth angle, yaw 의 정보를 담고 있다. 즉 감아져있는 원통을 한번 쭉 펼치는것과 마찬가지이다. 그리고 각 Element 에는 intensity 들을 가지고 있다. 여기에서 alpha p 는 yaw 라고 하며, beta p 는 pitch 라고 한다.

Waymo Dataset

Range Image

Waymo Dataset 같은 경우, 고해상도의 다양한 센서(Lidar / Radar / Lidar) 들로 Dataset 을가지고 있다. 주로 밀집된 도시중심이나 풍경, 그리고 날씨의 변화에 따른 다양한 환경에서 센서데이터를 가지고 있다. 내가 실제로 받은 데이터의 version 은 1.2 이다. 그리고 이 dataset 을 사용하려면, WaymoDataFileReader tool 를 사용해서, waymo dataset 을 읽은 이후에 객체의 형태로 들고 올 수 있다.

일단 간락한 설명을 하기위해서, training 만 봐보도록 하자. training 안에 여러개의 Camera Label Segment 가 존재하고, 그 하위에 Lidar / Radar / Camera 의 정보들을 가지고 있다. 예를 들어서 Top Lidar 를 가지고 오려면, 아래의 Python Code 를 사용하면 된다.

lidar_name = dataset_pb2.LaserName.Top
lidar = [obj for obj in frame.lasers if obj.name == lidar_name][0]

결국엔 이 Dataset 을 하기 위해선, Point Cloud Data 로 가지고 와야하지만, 여기에서 Point Cloud Data 이외에 표현하고 Visualize 를 하기 위해서는, 위의 Range Map 을 사용하면 된다. 여기에 Waymo Dataset 에서 한 Frame 당 구하기 위해선, 하나의 Frame 을 Matrix 로 변환이후에 reshape 을 해주면 shape (64, 2650, 4) 가 나온다. 아래의 코드는 Top Lidar 를 가지고 와서 Dimension 을 확인할수 있다.

lidar_name = dataset_pb2.LaserName.Top
lidar = [obj for obj in frame.lasers if obj.name == lidar_name][0]
if len(lidar.ri_return1.range_image_compressed) > 0:
  ri = dataset_pb2.MatrixFloat()
  ri.ParseFromString(zlib.decompress(lidar.ri_return1.range_image_compressed))
  ri = np.array(ri.data).reshape(ri.shape.dims)
  print(ri.shape)

Waymo Dataset 의 Range Image Structure 는 range, intensity, elogation, and vehicle position 을 가지고 있다. 그리고 Waymo dataset 에 elogation 값이 높고, intensity 가 낮은걸 날씨를 나타낼때 나타난다고 제시한다. 이 Range Image Structure 에서 내가 궁금한건 range 와 intensity 가 사용할것이다. 아래와 같이 Range Image 를 한번 확인 해보자.

Waymo Dataset 에서 사용된 Top Lidar 같은 경우 Scanning Lidar 이므로 Horizontal Field of View 는 360 degree 를 가지고 있다. 즉 360 / 2650 을 나눠보면 약 0.1358 만큼 degree 만큼 움직였으며, 이걸 Angular Resolution (min) 변환하면, 8.8 정도를 가지고 있다. 하지만 Vertical Field of View 에서의 Vertical Resolution 도 구하는게 필요하다. 즉 Minimum 부터 maximum inclination 을 확인해야므로, pitch 를 구해야한다.

Python 으로 구해보자면 아래와 같다. 여기서 max 와 min 을 빼줘서, 64 의 채널로 나눠준 각도를 구해주는 것이다.

lidar_calibration = [obj for obj in frame.context.laser_calibrations if obj.name == lidar_name][0]

min_pitch = lidar_calib.beam_inclination_min
max_pitch = lidar_calib.beam_inclination_max

vfov = max_pitch - min_pitch

pitch_res_rad = vfov / ri.shape[0]
pitch_res_deg = pitch_res_rad * 180 / np.pi

Range Image 이의 Range 의 Value 값들은, 환경속의 특정 포인트까지의 거리를 2D 이미지로 담아냈기때문에, 센서부터 거리(distance) 를 말한다. 근데 여기에서 min = -1 일때가 있는데 geometrically 하게 make sense 하지 않는다. 그래서 Filter 를 한번 해줘야한다. 자세한 내용은 Waymo Dataset Paper 을 참고하자. 일단 이부분을 구현한 부분은 아래와 같다.

def load_range_image(frame, lidar_name):
  lidar = [obj for obj in frame.lasers if obj.name == lidar_name][0]
  ri = []
  if len(lidar.ri_return1.range_image_compressed) > 0: # use first response
      ri = dataset_pb2.MatrixFloat()
      ri.ParseFromString(zlib.decompress(lidar.ri_return1.range_image_compressed))
      ri = np.array(ri.data).reshape(ri.shape.dims)
  return ri

def get_max_min_ranage(frame, lidar_name):
  ri = load_range_image(frame, lidar_name)
  # ri[:, :, 0] -> range
  # ri[:, :, 1] -> intensity
  ri[ri<0]=0.0
  print('max. range = ' + str(round(np.amax(ri[:,:,0]),2)) + 'm')
  print('min. range = ' + str(round(np.amin(ri[:,:,0]),2)) + 'm')

그 이후에 Range Image 를 Visualize 하기 위해서는 Range 의 Channel 을 살펴보아야한다. Range Image Structure 의 Shape 은 (64, 2650, 4) 이였다. 여기에서 할수 있는 방법은 Normalize 를 한이후에 8 bit grayscale image 로 다루어야한다. 그 이후에 OpenCV 를 사용해서 image_range 를 볼수 있다. 하지만 Range Image 는 Lidar 의 Full Scan 이미지를 가지고 있으므로, 차가 바라보는 방향만, ROI 를 정해줄수 있는게 필요하다. 여기에 Waymo Dataset Paper, 명시된것 처럼 -45 ~ +45 도 만큼을 잘라낼 필요가 있다.

아래의 코드는 위의 내용을 기반으로 -45 도와 45 도의 Range 를 가지고 Crop 한 Image 를 구하는 방식이다. range_image 의 결과의 이미지가 이싿.

def visualize_range_image_channel(frame, lidar_name):
  ri = load_range_image(frame, lidar_name)
  ri[ri < 0] = 0.0

  ri_range = ri[:, ;, 0]
  ri_range = ri_range / (np.amax(ri_range) - np.amin(ri_range))
  image_range = ri_range.astype(np.uint8)

  ri_center = int(image_range.shape[1] / 2)
  image_range = image_range[: , ri_center - int(image_range.shape[1] / 8): ri_center + int(image_range.shape[1] / 8)]

  cv2.imshow('range_image', image_range)
  cv2.waitKey(0)

Intensity

Range Image 이외에 살펴봐야하는 부분이 바로 Intensity 부분이다. 결국 Lidar 는 64개의 Channel 을 쏘았을때, 물체에 부딫쳐서 돌아왔을때의 색깔을 결정하기 위한 Intensity 들을 Return 한다. 그리고 이러한 Range Intensity 를 가지고, 우리가 Detection Algorithm 을 사용할수 있게끔 Point Cloud 가 나오게 된다.

일단 min-max normalization 으로 그렸을때, 아래와 같이 나올수 있다. 이렇게 나온 이유는 reflective material 을 가지고 있는건 그대로 Intesnity 를 Return 할 경우가 있는데, 이때 intensity 가 엄청 밝은것과 어두운것은 확죽이는데 적당하게 밝은 애들은 Noise 들을 더키우기 때문이다. 그래서 heuristic 방법을 사용하면, 아래처럼 scaling 을 할수 있다. 이때 사용한 scaling 방법은 Contrast adjustment 이라고 한다.

ri_intensity = np.amax(ri_intensity)/2  *ri_intensity*  255 / (np.amax(ri_intensity) - np.amin(ri_intensity))

그래서 위의 내용을 적용하면 아래와 같이 사진이나오는데, 차량의 licence plate 가 reflective 하기 때문에 차량의 뒷편에 intensity 가 높은걸 확인할수있다.

def visualize_intensity_channel(frame, lidar_name):
  ri = load_range_image(frame, lidar_name)
  ri[ri < 0] = 0.0

  # map value range to 8 bit
  ri_intensity = ri[:, :, 1]  # get intensity
  ri_intensity = ri_intensity * 255 / (np.amax(ri_intensity) - np.amin(ri_intensity))
  img_intesnity = ri_intensity.astype(np.uint8)

  deg45 = int(img_intensity.shape[1] / 8)
  ri_center = int(img_intensity.shape[1]/2)
  img_intensity = img_intensity[:,ri_center-deg45:ri_center+deg45]

  cv2.imshow("img", img_intensity)
  cv2.waitKey(0)

다시 말해서, 우리가 결국 range_image 로 부터 구하고 싶은건 Point cloud 를 return 하는 거다. range image 에서 point cloud 로 변경하려면, range image 에서 어떠한 point 를 spherical coordinate 에서 world coordinate 로 변경해야한다.

Spherical Coordinates

일단 Range Image 로 부터 Point Cloud Data 를 가지고오려면, 위에 했던 내용을 결국은 사용해야하며, Calibration Data 를 Waymo Dataset 에서 가져와야한다. 그리고 결국엔 vehicle 이 x axis 로 보게끔 range image 를 correction 을 거쳐야한다. 이때, extrinsic calibration matrix 를 가지고 와야한다.

아래는 Python range_image 를 Point Cloud 변경하는 코드이다.

  1. calibration 을 하기 위해서, calibration data 를 가지고 온다.
  2. 그 data 에서 extrinsic matrix 를 가지고 와서, azimuth 를 구해준다. 이때 값을 구할때, [1, 0] 과 xetrinsic[0, 0], spherical coordinates 에서 world coordinate 으로 변경한 Y 와 X 의 값이다.
  3. 실제 고쳐야되는 azimuth 가 있었다면 -180 부터의 180 까지에서 corrected 된걸 연산해준다.
  4. Corrected 된걸 가지고, World Coordinates X, Y, Z 를 연산해줘서, 센서의 위치를 파악한다.
  5. Sensor 위치가 나오면, extrinxisc 과 xyz_sensor 를 matrix multiplication 을 통해서 ego coordinate system 으로 변경한다.
  6. Point Cloud Data 를 (64, 2560, 4) 변경시켜서, 일단 거리가 0 보다 작은것들은 row 로 masking 을 시켜서, filtering 을 한이후 0:3 까지의 값들은 가지고 온다.(3 의값은 1 –> Homogeneous Coordinates)
  7. Point Cloud Data 를 그린다.
def range_image_to_point_cloud(frame, lidar_name):
  ri = load_range_image(frame, lidar_name)
  ri[ri < 0] = 0.0
  ri_range = ri[:, :, 0]

  # load calibration data
  calibration = [obj for obj in frame.context.laser_calibrations if obj.name = lidar_name][0]

  # compute vertical beam inclination
  height = ri_range.shape[0]
  inclination_min = calibration.beam_inclination_min
  inclination_max = calibration.beam_inclination_max
  inclination = np.linspace(inclination_min, inclination_max, height)
  inclination = np.flip(inclinations)

  width = ri_range.shape[1]
  extrinsic = np.array(calibration.extrinsic.transofrm).reshape(4,4)
  azimuth_corrected = math.atan2(extrinsic[1,0], extrinsic[0, 0])
  azimuth = np.linspace(np.pi, -np.pi, width) - azimuth_corrected

  azimuth_tiled = np.broadcast_to(azimuth[np.newaxis,:], (height,width))
  inclination_tiled = np.broadcast_to(inclinations[:,np.newaxis],(height,width))

  x = np.cos(azimuth_tiled) * np.cos(inclination_tiled) * ri_range
  y = np.sin(azimuth_tiled) * np.cos(incliation_tiled) * ri_range
  z = np.sin(inclination_tiled) * ri_range
  # transform 3d points into vehicle coordinate system
  xyz_sensor = np.stack([x,y,z,np.ones_like(z)])
  xyz_vehicle = np.einsum('ij,jkl->ikl', extrinsic, xyz_sensor)
  xyz_vehicle = xyz_vehicle.transpose(1,2,0)

  idx_range = ri_range > 0
  pcl = xyz_vehicle[idx_range,:3]

  pcd = o3d.geometry.PointCloud()
  pcd.points = o3d.utility.Vector3dVector(pcl)
  o3d.visualization.draw_geometries([pcd])

  pcl_full = np.column_stack((pcl, ri[idx_range, 1]))

  return pcl_full    

아래는 한 Frame 의 Point Cloud 를 관찰한 결과이다.

Point Cloud Data

Resource

DirectX11 - Texturing

DirectX11 - Texturing

일단, Texturing 을 하기 위해서, Shader Programming 해야된다. 일단 Texturing 을 하려면 Vertex Shader 에서 해야되는지, Pixel Shader 에서 해야되는지가 고민이 되는데, 일단 Microsoft 공식문서에서는 Pixel Shader 에서 하라고 명시가 되어있다.

그러면 PixelShader 의 Program 을 잠깐 봐보자. 일단 GPU 에서 Texture Image 를 Texture2D 로 받아 올수 있다. 그리고 앞에서 잠깐 언급했듯이, Texture Image 안에서 색깔 값을 가져오는 걸 Sampling 이라고 했었다. 그래서 SamplerState 도 사용해야한다. 여기서 register 일때의 t0s0 이 있다. t0 같은 경우, Texture 일때, 그리고 index 는 0, 그리고 sampler 일때는 s 그리고 index 는 0. 자세한건, 이 Resource 를 활용하자.

Texture2D g_texture0 : register(t0);
SamplerState g_sampler : register(s0);

cbuffer PixelShaderConstantBuffer : register(b0) {}

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
    float2 texcoord : TEXCOORD;
}

float4 main(PixelShaderInput input) : SV_TARGET
{
    return g_texture0.Sample(g_sampler, input.textcoord);
}

hlsl 에서 이렇게 작성을 했다고 한다면, CPU 에서 보내주는 Member 도 만들어줘야한다. 여기서 ShaderResourceView 같은 경우, Texture 의 Resource 로 사용하는데, 왜 구지? View 가 필요하지라고 생각할수 있다. 사실 View 라는건 Texture 자체를 RenderTarget 로 사용할수 있기 때문이다. 같은 Memory 를 사용하더라도, RenderTarget 으로 설정이 가능하며, 이 Shader 를 가지고, 다른 쉐이더로 Input 으로 넣어줄때 ResourceView 로 넘겨줄수있다.

ComPtr<ID3D11Texture2D> m_texture;
ComPtr<ID3D11ShaderResourceView> m_textureResourceView;
ComPtr<ID3D11SamplerState> m_samplerState;

주의해야될점은 한 Texture 를 한 Shader 안에서 동시에 Resource 와 RenderTarget 으로 사용할수 없다. 그래서 Texture 가 두개가 있다고, 가정하면, Res1 -> Shader -> RT2 = Res2 -> Shader -> RT1 이렇게 사용이 가능하다.

Resource

After Life

가끔씩 나는 미국이 그리울때가 많다. 남들이 한국어를 잘한다고 생각하지만, 나는 한국어가 어려운 언어중에 하나라고 생각한다. 그리고, 나는 그냥 영어를 듣기를 좋아하고 말하는것도 한국어에비해 더 자유로움을 많이 느낀다. 그래서 오랜만에 Netflix 를 켜서, 재밌는 Show 를 찾아봤다.

After Life 의 Creator 이자, Main Character 인 Ricky Gervais 는 정확히 많이 알진 못하지만 Granny 에서 연설을 한것과 Standing Commedy 를 하는걸로 유명하다. 뭔가 직설적이면서도, 영국인의 Joke 의 느낌을 그대로 물들여진 그런 캐릭터이다. 이 Show 에서도 변하지 않았다. 솔직히 말하면, Season 1, 2, 3 를 보면서 cunt 나 twat 이라는 단어밖에 잘 기억이 안난다. 아무튼, 여기서 Main Character Tony 는 Cancer 로 인해서, Wife 를 잃고 담은 스토리이다. 작은 마을 Tambury 에서 Local newspaper 에 Story 를 쓰는 Jornalist 이다. 매 episode 마다 나오는 Wife 와의 추억을 담은 영상을 보면서, 그 wife(Lisa) 를 못잃고, 어떡하면 자살하지 않을지에 대한 내용이지만, Lisa 가 남겨놓은 Video 에서는 사람에게 친절을 베풀어라, 너가 나없어도 행복했으면 좋겠다 이런 내용들이 담겨져있다.

이런 Video 의 내용을 보면서, Tony 는 매번 죽을려고 노력하지만, 어떤 어느 순간에 그걸 막는 장면들이 보이고, 여러친구들이 행복했으면 좋겠다라는 말들이 많이 나온다. 뭔가 동정심을 유발하면서도 이겨낼려는 Tony 의 모습을 보면, 어찌됬든 현생을 올바르게 맞춰가려고 노력하는 사람인것 같았다. 물론 여러 Character 를 가진 사람들이 나오는데, Tony 에게 의존하는 사람들도 있고, Tony 가 의존하는 사람들이 보인다. 그리고 Lisa 와 Tony 가 키우던 개 가 있는데 몇번이나 Tony 를 살리려고 정말 애를 쓴다. 역시 강아지라는 존재는 위험할때를 정말 잘 캐치하고, 마치 사람처럼 행동하는걸 정말 잘표현한것 같았다.

정말 이 SHow 를 보면서 생각보다 Shameless 가 생각이 많이들었다. Shameless 같은 경우는 Wow they are so miserable that I found myself happiness 이런 말이 나올정도로 뭔가 나에게 위로를 줬다고 한다면, 이 Show 같은 경우는 ㅈ아 올바르게 산다는건 힘들고, Memory is just a super power 라는 생각이 든다. 맞다! 정말 사랑했던 사람은 기억속에 산다는점이, 그리고 그 기억속에 사는 사람이라면 어떠한것도 이겨낼수 있다? 이런 느낌을 정말 많이 받았다.

물론 British Drama 는 나의 최애는 아니다. 욕도 뭔가 더 상스럽고, 더 느낌이 강하다. 하지만 가끔씩보면 되게 Refresh 가 되었다. 그리고 Ricky Gervais 는 정말 talented 한 사람이기 때문에, 그리고 제작자와 Main Character 가 이 Show 에 잘들어났기 때문에, 정말 좋은 Drama to watch 이다.

HLSL Introduction

Shader Programming

Programming 언어에서도 여러가지 종류가 달라지듯이, Pipeline 안에서 각각의 Stage 마다, 안보이는 Shader Programming 을 해줘야 하고, 언어도 다른 종류가 있다.

아래의 그림을 참고해서 그림을 보자면, IA Stage 에서 Memory Resource (Buffer, Texture, Constant Buffer) 에서, IA 로 들어간 이후에 아래쪽으로 쉐이더를 통과해서 진행한다. 참고로, 이때 Memory Resource 에서 IA 로 들어가는 데이터의 배치상태(layout) 이라고한다.

DirectX 에서는, HLSL(High Level Shader Language) 를 사용한다. HLSL 에 들어가보면, 이런 구문이 보인다. HLSL is the C-like high-level shader language that you use with programmable shaders in DirectX. C Language 하고 비슷하다. 그리고 HLSL 로 Compute Shader 할때는 Direct Machine Learning 을 사용하라고 한다.

즉 일단 Shader Programming 이란, 결국에 GPU 에서 작동하는 Programming 이라고 생각하면 편할것 같다.

일단 기본적으로 아래와 같이 Shader Programming 을 할수 있다. C 나 C++ 처럼, 제일 처음에 시작되는 부분이 바로 main 부분이다. Shader 에도 main 이 따로 있다. 일단 Shader 의 종류가 여러가지가 있다. 예를들어서 Vertex Shader 가 있고, Pixel Shader 등등 있는데, 서로 연관성은 없으며, 하나의 독립적인 Module 이라고 생각하면 편하다, 독립적인 Module 이기 때문에, Compile 도 따로한다. 하지만 data 는 Share 할수 있다.

예를 들어서 아래의 HLSL Programming 을 봐보자.

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 color : COLOR0;
}

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 color : COLOR;
}

PixelShaderInput main(VertexShaderInput input)
{
    PixelShaderInput output;
    float4 pos = float4(input.pos, 1.0f);
    pos = mul(pos, model);
    pos = mul(pos, view);
    pos = mul(pos, projection);

    output.pos = pos;
    output.color = input.color;

    return output;
}

// pixel shader
float4 main(PixelShaderInput input) : SV_TARGET
{
    return float4(input.color, 1.0);
}

위의 코드 같은 경우 float3 pos : POSITION 이라고 나와있는데 colon(:) 다음에 나오는건 Sematics 인데, 어떤 Parameter 종류다라는것을 명시한다. 자세한건 Shader Semantics 참고하자.

그렇다면 Vertex Shade.r 와 Pixel Shader 를 조금 더 알아보자. 위의 코드에서 __ShaderInput 이라는 구조체가 보인다. 그렇다면 Pipeline 에서 Output 도 존재할수 있는데, 여기서 Vertex Shader 의 Output 이 Interpolation 을 거쳐서 Pixel Shader 의 Input 이 되기때문에 따로 명시하진 않았다.

그리고 위의 PixelShaderInput 구조체에서 Vertex Shader 와 비슷하게 생겼지만 SV (System-value semantics)라는게 들어가 있는데, 이 이유는 Shader의 Input 으로 들어온다 라는걸 표시한다.

Pixel Shader 에서는 Graphics Pipeline 안에서 제일 마지막에 위치해있기때문에 Semantics 가 SV_TARGET 즉 Render 를 할 Target 이라는 semantics 를 넣어주어야한다.

Shader 에서 Constant Buffer 도 거쳐서 계산하게끔 도와줘야한다. 그러기 때문에 이것에 필요한 문법도 따로 명시해줘야한다. 아래와 같이 표현 할수 있는데 여기서 register 안에 b0 이라는 인자가 들어간게 보인다. 이건 Register Type 인데 b 일 경우는 Constant Buffer, t 일때는 Texture buffer, c 일 경우 Buffer offset 등 여러가지 타입이 존재한다.

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
}

Resource

COMPTR - Window Programming & DirectX11 Initialization

DirectX 의 공부에 앞서서, c++ 에서 자주 사용하는 shared_ptr 이있는데, Microsoft 에서 제공하는 Microsoft::WRL::ComPtr is a c++ template smart-pointer for COM(Component Object Model) objects that is used extensively in Winodws Runtime (WinRT) C++ Programming. 이라고 한다.

일단 사용할때 아래와 같이 정의할수 있다.

Microsoft::WRL::ComPtr<ID3D11Device> device; // COM Interface
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;

만약 이러한 ComObject 와 관련된걸, brute-force 하게, C++ 로 만든다면, 이렇게 표현을 할것이다. std::shared_ptr<ID3D11Device> device = make_shared<ID3D11Device>(...);1D3D11Device *device = nullptr;. 그리고 make_shared or new 를 하는게 아니라, 용도에 맞게, 지정되어있는 함수로 만들어야한다.

Adapter 가 만들어졌다고 하는 가정하에, DirectX11 의 Device 와 Device Context 를 만들어줘야한다. DirectX 11 의 Device 란 Object 이며, 이 device 는 Desired Adaptor 에서, DirectX11 Render 가 사용할 object 를 생성하는 역활을 하고, 대부분 initialize 를 할때 개체가 생성되고, 소멸된다. context 는 사실 device context 인데, 이 역활은 어떤 Commands 를 submit 해주고, adaptor 가 실행을 시켜준다. 예를 들어서 Render Command 나, 어떠한 Transfer 할 Data 를 update 하는데 사용된다 (주로 Rendering Process 중일때…) 즉 둘다 Interface 라고 생각하면된다. 그래서 생성될때, ID3D11DeviceID3D11DeviceContext interface 를 통해서 생성된다.

일단 Device 에 관련되서 생성을 하려면 아래와 같이 해야한다. 그리고 필요한건 context 이다.

Microsoft::WRL::ComPtr<ID3D11Device> device; // COM Interface
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;

const D3D_FEATURE_LEVEL featureLevels[2] = {
        D3D_FEATURE_LEVEL_11_0,
        D3D_FEATURE_LEVEL_9_3};

D3D_FEATURE_LEVEL featureLevel;

HRESULT hr = D3D11CreateDevice(
    nullptr,
    D3D_DRIVER_TYPE_HARDWARE,
    0,
    creationFlags,
    featureLevels,
    ARRAYSIZE(featureLevels),
    D3D11_SDK_VERSION,
    &device,
    &m_d3dFeatureLevel,
    &context);

Device 가 생성된다고 해서, Render 를 한다는 말은 아니다. DirectX11 에서는 swapchain DXGI 를 통해서 만들어줘야한다. SwapChain 이란 backbuffer 의 개수를 관리하고, 한 buffer 마다 access 를 가능하다, 즉 이때 모니터에 갈거 따로 하나 backbuffer 에 그릴거 하나 이렇게 cover 를 한다. 그래서 아래의 그림처럼 SwapChain 안에 Buffer 를 끌어다가 Texture 를 설정해주고, 실제 Render 할 TargetView 만들어준다음에 기다리면서 switching 을 할수 있게 한다.


Introduction to Rendering Pipeline in D11

일단 어떤 기하의 정보를 정의한 이후로 Vertex Buffer 를 만들어줘야된다. 여기에서 Buffer 같은 경우는 GPU Memory 를 준비한다. 그래서 Vertex 의 정보(정점별 위치 및 Textrue Data) 를 담는 VertexBuffer 가 있고, 그 Vertex 가 어떤 순서로 이루어져야 하는지는 IndexBuffer(Rendering 할 모형의 Index 를 제공, 동일한 정점을 재사용) 에다가 넣어준다. 또 Constant BufferMVP (Model, View, Projection) 으로 정의 되어있다. Constant Buffer 의 의미상으로는 어떤 static data 를 가지고 있는데, 이게 pixel shader 호출에 필요한 정적데이터를 또는 모든 버텍스 및 Pixel 를 가르친다. 즉 Buffer 들을 CPU 가 정의했다가, GPU 에 넘기는 용도로 사용된다.

일단 구현을 봐보자면,


Resources

Implement Initialization in Direct3D

const D3D_DRIVER_TYPE driverType = D3D_DRIVER_TYPE_HARDWARE;

Lighting

Lighting in Rasterization

Ray Tracing 과 마찬가지로, Rasterozation Pipeline 에서도 Lighting 의 종류는 Directional Light / Point Light / Spotlight 이 있다. 역시 이것도 Unreal Light Type 의 일부분이다. (참고: Unreal Light Type 은 SkyLight 과 Rect Light 이 존재한다.)

일단 Shading 부분에서 구현했듯이, Directional Light 은 구하긴 쉽다. 여기에서는 Point Light 과 Spotlight 을 구현해보려고 한다.

Point Light

Point Light 의 Physical 한 부분은, 전구로 묘사 하는게 제일 알맞다. 어떤 한 빛의 지점에서 구의 형태로 부터 여러 방면으로 빛이 나간다.

아래의 그림을 참고하자. 어떠한 Arbitary point P 로 부터, Point Light 의 Origin Q 로 부터 빛이 퍼져나간다. 그래서 Light 을 구하는 방법은, 우리가 보는 시점 P 로부터 Q 의 Vector 즉 Q - P 의 Vector 로 표현이 가능하다. 그래서 Direction 을 찾자면 아래의 그림 처럼 공식이 성립이된다.

그리고 Light 의 방향 벡터는 위와 같이 구했지만, 실제 Light 의 세기인 Intensity 를 Physical Level 에서 그럴싸하게 보이려면 Attenuation Function 을 사용해야한다. Light 의 세기 가 거리에 따라서 약해지기 대문에 I(intensity) = I(initial) / d^2 이렇게 정의 할수 있고, 이건 HDR(High dynamic range) 에서 사용되고 tonemapping 에 사용된다. 근데 쉽게 구현 가능하는거는 아래와 같은 함수이다. saturate 함수인데 아래와 같이 표현이 가능하다. 일단 fallofStart 까지는 1 이라는 constant value 가 적용되고, 그리고 fallOfStart 와 fallOfEnd 의 사이는 0 까지의 직선이고, falloffEnd 부터는 0 이된다.

구현하는 방법은 아래와 같다.

Spot Light

Spotlight 을 받는다라고 하는 장면을 생각해보면, 어떤 무대에서 배경은 다 까맣고, 그 Spot Light 를 받는 인물만 빛이 들여오는걸 상상할수 있다. SpotLight 은 Point Light 과 비슷하다. 하지만 square 이 붙는다. 아래의 Figure 를 보면 P 는 빛을 받는 지점이고 Q 는 spotlight source 이다. P 를 봤을때 SpotLight 의 Cone 안에 있다는걸 확인 할수 있는데 이걸 결정할수 있는게 phi max 보다 D 와 -L 의 dot product 를 했을때 각도가 적다는걸 확인 할수 있다. 그리고 이 Cone 에서의 특징점은 Cone 의 Q 지점에서 직선으로 being lit 했을때의 지점이 제일 밝고, 그 지점으로 부터 멀어질수록, 즉 Phi max 일때는 빛의 intensity 가 0 이 된다. 그래서 max(cos(phi), 0 )^5 라고 말할수 있고, 결국 이거는 max(-L dot d, 0)^5 라고 볼수 있다.

C++ 구현

struct Light {
    vec3 strength = vec3(1.0f);
    vec3 direction = vec3(0.0f, -1.0f, 0.0f);   // directional/spot light only
    vec3 position = vec3(0.0f, 1.0f, 0.5f);     // point/spot light only
    float fallOffStart = 0.0f;                  // point/spot light only
    float fallOffEnd = 1.8f;                    // point/spot light only
    float spotPower = 0.0f;                     // spot light only
};

struct VSInput {
    vec3 position;
    vec3 normal;
    vec3 color;
    vec2 uv;
};

struct VSOutput {
    vec3 position;
    vec3 normal;
    vec3 color;
    vec2 uv;
};

vec3 BlinnPhong(vec3 lightStrength, vec3 lightVec, vec3 normal, vec3 toEye,
                Material mat) {

    vec3 halfway = normalize(toEye + lightVec);
    vec3 specular =
        mat.specular * pow(glm::max(dot(halfway, normal), 0.0f), mat.shininess);

    return mat.ambient + (mat.diffuse + specular) * lightStrength;
}

vec3 ComputeDirectionalLight(Light L, Material mat, vec3 normal, vec3 toEye) {
    vec3 lightVec = -L.direction;

    float ndotl = glm::max(dot(lightVec, normal), 0.0f);
    vec3 lightStrength = L.strength * ndotl;
    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

float Saturate(float x) { return glm::max(0.0f, glm::min(1.0f, x)); }

float CalcAttenuation(float d, float falloffStart, float falloffEnd) {
    // Linear falloff
    return Saturate((falloffEnd - d) / (falloffEnd - falloffStart));
}

vec3 ComputePointLight(Light L, Material mat, vec3 pos, vec3 normal,
                       vec3 toEye) {
    vec3 lightVec = L.position - pos;
    float d = length(lightVec);
    if (d > L.fallOffEnd)
        return vec3(0.0f);

    lightVec /= d;

    float ndotl = glm::max(dot(lightVec, normal), 0.0f);
    vec3 lightStrength = L.strength * ndotl;

    float att = CalcAttenuation(d, L.fallOffStart, L.fallOffEnd);
    lightStrength *= att;

    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

vec3 ComputeSpotLight(Light L, Material mat, vec3 pos, vec3 normal,
                      vec3 toEye) {
    vec3 lightVec = L.position - pos;
    float d = length(lightVec);
    if (d > L.fallOffEnd)
        return vec3(0.0f);

    lightVec /= d;

    float ndotl = glm::max(dot(lightVec, normal), 0.0f);
    vec3 lightStrength = L.strength * ndotl;

    float att = CalcAttenuation(d, L.fallOffStart, L.fallOffEnd);
    lightStrength *= att;

    float spotFactor = glm::pow(glm::max(dot(-lightVec, L.direction), 0.0f), L.spotPower);
    lightStrength *= spotFactor;

    return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

// Vertex Shader
VSOutput VertexShader(const VSInput vsInput) {
    VSOutput vsOutput;

    vsOutput.position =
        RotateAboutX(
            RotateAboutY(vsInput.position * constants.transformation.scale,
                         constants.transformation.rotationY),
            constants.transformation.rotationX) +
        constants.transformation.translation;
    
    vsOutput.normal = RotateAboutX(
        RotateAboutY(vsInput.normal, constants.transformation.rotationY),
        constants.transformation.rotationX);

    return vsOutput;
}

// Pixel Shader
struct PSInput {
    vec3 position;
    vec3 normal;
    vec3 color;
    vec2 uv;
};

vec4 PixelShader(const PSInput psInput) {

    vec3 eye = vec3(0.0f, 0.0f, -1.0f); // -distEyeToScreen
    vec3 toEye = glm::normalize(eye - psInput.position);

    vec3 color;

    if (constants.lightType == 0) {
        color = ComputeDirectionalLight(constants.light, constants.material,
                                        psInput.normal, toEye);
    } else if (constants.lightType == 1) {
        color = ComputePointLight(constants.light, constants.material,
                                  psInput.position, psInput.normal, toEye);
    } else {
        color = ComputeSpotLight(constants.light, constants.material,
                                 psInput.position, psInput.normal, toEye);
    }

    return vec4(color, 1.0f);
}

Reference

DirectX12

Shading

Shading

Vertex shader 에서 주로 shading 파트를 한다, 요즘엔 그래픽 카드가 좋아서, pixel shader 에서 종종한다고 한다. 일단 shading 이란 조명과 재질을 고려해서 색을 결정하는 작업을 말한다. 주로 방법론은 두가지 방법이 있는데 Vertex shader 에서 vertex 의 색을 결정한 이후에 pixel shader 로 보내줘서 pixel shader 에서 interpolation 을 하는 방법이 있고, ㅏ나머지 방법은 shading algorithm 을 pixel shader 계산하는 방법이 있다. 두가지 방법을 구지 고려했을때 정점의 개수가 확실히 적기 때문에 shading 을 Vertex shader 에서 하는 편이다. Ray Tracing 에서 구현한 Phong shading model 의 개선된 Blinn-Phong Shading 을 알아보자.

Material

일단 Shading 을 하기 앞서, 조명에 관련된걸 잠깐 언급하려고 한다. 조명에도 2 가지가 있는데, Local Illuimination (직접 조명) 이 있고, Global Illumination (간접조명)이 있다. 이 두가지의 차이는 직접 조명 같은 경우는 직접적으로 조명을 주기때문에 빛춘곳만 shading 이 생기는거고, Global illumination 은 그 빛에만 조명효과가 일어나는게 아니라 빛이 분배되는것처럼 구현이 된다.

예를 들어서 빛이 물체를 비췄을때, 조명과 물체가 비스듬하게 비췄을때의 그 suface 를 Lambertian Surface 라고 하는데, 이것도 마찬가지로 조명과 물체가 수직일때 물체에 받는 intensity 가 강할껀데 이공식이 I(diffuse) = Kd * I(light) * cos(theta) 라 정리가 된다. Lambertian Surface 에서 정의하는게 어떤 조명이 표면이 울퉁불퉁하는 곳에 부딫쳤을때, 난반사가 일어나는데 이때 난반사의 모양이 반쪽의 구의 모형으로 균일하게 반사하는걸 표현한다. 여기에서 더 extend 를 하자면 반구의 크기가 결국에는 입사각에 따라서 달라진다 라는 말이 된다.

결국엔 물체의 색깔을 결정하기 위해선, Reflection 이 중요하고, Reflection 을 정의하려면 물체의 표면을 고려해야한다. 그래서 Specular 과 Diffuse 는 물체의 표면을 생각했을때, 떼어낼수 없는 존재이다. 그 예는 아래와 같이 Image 를 참고하자

Blinn-Phong Shading

일단 Phong 보다 개선된점은 바로 계산의 속도 문제이다. Phong 모델에서는 R dot V 를 했었다. 하지만 Blinn Phong 에서는 N dot H 를 한다는 점이 포인트이다. 이때 H 는 Halfway Vector 라고 하는데 왜 Half 냐면 N 과 L 사이의 중간지점인 Vector 이기 때문이다. H 같은 경우는 L + V / ||L + V|| 로 구할수 있다.

역시 Phong 과 Blinn-Phong 을 비교 해봐야 어떤게 좋은지 알수 있다.

OpenGL 일단 Phong Model 은 Reflection vector 와 View Vector 의 각도가 크기 때문에 제곱을 했을때 더 날카롭게 표현되기때문에 Phong model 을 봤을때 directional light 가 더 강하게 보인다. 하지만 Blinn-phong 같은 경우 halfway vector 와 Normal vector 의 각도가 작기때문에 제곱을 해도 부드럽게 나오는걸 확인할수 있다.

계산에서 중요한점은 Point 같은 경우는 이동이 가능하지만, Vector 는 이동이 불가능하다. 이 점이 물체가 이동을 할때 Point 도 옮겨줘야하지만, Normal Vector 도 같이 움직여줘야한다는 부분이다.

그렇다면 C++ 코드로 GPU 에서 해야될 shader 를 잠깐 구현한다고 하면, 아래와 같다.

#include <glm/glm.hpp>
#include <vector>

// Helper function for rotating about certain axis 
vec3 RotateAboutZ(const vec3 &v, const float &theta) {
    return vec3(v.x * cos(theta) - v.y * sin(theta),
                v.x * sin(theta) + v.y * cos(theta), v.z);
}

vec3 RotateAboutY(const vec3 &v, const float &theta) {
    return vec3(v.x * cos(theta) + v.z * sin(theta), v.y,
                -v.x * sin(theta) + v.z * cos(theta));
}

vec3 RotateAboutX(const vec3 &v, const float &theta) {
    return vec3(v.x, v.y * cos(theta) - v.z * sin(theta),
                v.y * sin(theta) + v.z * cos(theta));
}

struct Transformation {
  vec3 scale = vec3(1.0f);
  vec3 translation = vec3(0.0f);
  float rotationX = 0.0f;
  float rotationY = 0.0f;
  float rotationZ = 0.0f;
};

struct Material{
  vec3 ambient = vec3(0.1f);
  vec3 diffuse = vec3(1.0f);
  vec3 specular = vec3(1.0f);
  float shininess;
};

sturct Light{
  vec3 strength = vec3(1.0f);
  vec3 direction = vec3(0.0f, -1.0f, 0.0f);
};

struct Constants {
  Transformation transformation;
  Light light;
  Material material;
} constants;

struct VSInput{
  vec3 position;
  vec3 normal;
  vec3 color;
};

struct VSOutput{
  vec3 position;
  vec3 normal;
};

vec3 BlinnPhong(vec3 lightStrength, vec3 lightVec, vec3 normal, vec3 toEye,
              Material mat){
  vec3 halfway = normalize(toEye + lightVec);
  vec3 specular = mat.specular * pow(glm::max(dot(halfway, normal), 0.0f), mat.shininess);
  return mat.ambient + (mat.diffuse + specular) * lightStrength;
}

vec3 ComputeDirectionalLight(Light L, Material mat, vec3 normal, vec3 toEye){
  vec3 lightVec = -L.direction;
  float ndotl = glm::max(dot(lightVec, normal), 0.0f); // exception for negative value
  vec3 lightStrength = L.strength * ndotl;
  return BlinnPhong(lightStrength, lightVec, normal, toEye, mat);
}

// Vertex Shader
VSOutput MyVertexShader(const VSInput vsInput){
  VSOutput vsOutput;

  vsOutput.position = RotateAboutX(
    RotateAboutY(vsInput.position * constants.transformation.scale,
      constants.transformation.rotationY),
      constants.transformation.rotationX) + constants.transformation.translation;

  vsOutput.normal = RotateAboutX(
    RotateAboutY(vsInput.normal, constants.transformation.rotationY),
      constants.transformation.rotationX);

  return vsOutput;


// Pixel Shader 
struct PSInput {
  vec3 position; 
  vec3 normal;
  vec3 color;
  vec2 uv;
}

vec4 MyPixelShader(const PSInput psInput){
  vec3 eye = vec3(0.0f, 0.0f, -1.0f);
  vec3 toEye = glm::normalize(eye - psInput.position);
  vec3 color = ComputeDirectionalLight(constants.light, constants.material,
                                      psInput.normal, toEye);

  return vec4(color, 1.0f);
}
}

이런식으로 구현이 가능하다. 여기서 Minor 한점은 glm::max(…) 를 통해서 각이 90 도 이상 일때 빛의 세기가 덜받으면 덜받았지 음수가 될수 없다는 점과. Blinn-phong algorithm 을 사용해서 halfway vector 의 방향을 구한다음에 color 값을 return 해주는 함수가 있다는 점을 한번 참고하자.

Resource

Pagination


© 2021. All rights reserved.