사실 나는 자율 주행과는 조금 가깝지도 멀지도 않은 직종에서 Simulator 와 그의 부가 되는 내용들을 개발을 하고 있다. 물론 실제 차량에 쓸수 있을지는 전혀 알지도 모르고, on-time 으로 뭔가를 할수 있다는것과도 거리가 멀지만, Autonomous Vehicle Simulator 이기에 또 가깝기도 하다. 그래서 뭔가 개요? Perception 에 대한 대체적인 것 들을 배우기 위해서 이 강의를 듣게 됬다. 그리고 SOS Lab 의 이용이님과 bitsensing 이재은, Seoul Robotics 의 이한빈님들의 알려주는 이야기를 짧게 또는 개요를 듣고 싶어서, 이 강의를 선택하게 되었다.
대부분의 이야기는 Perception 에 살짝 치우쳐져 있다는 느낌이 있었고, 인지 부터 시작해서 제어까지의 총 통틀어 하나로 나오는 Fullstack 은 어떻게 진행하고 있는지와 그리고 자율 주행에서의 큰 문제점을 어떻게 해결할지 (Perception Module 에서 부터 센서 퓨전 한 이후에 Command 까지 보내주는데에 있어서의 Latency 를 줄여야한다.) 등을 이야기 했고, Data-Driven 이 rule-based 보다 좋을수 밖에 없다라는 말도 조금 받아들이기는 쉽지 않았다.
일단 센서의 종류 부터 보자면, 아래와 같다. 딱히 HDmap 은 센서의 종류라고 말은 할수 없지만, 또 자율주행에 있어서 빼놓을수 없는 것이다.
Camera
Lidar
Radar
GPS / IMU
Hdmap
즉, 자율 주행 자동차가 주변 환경을 인지해 주행에 필요한 정보들을 획득 및 취득하고, 그리고 이렇게 다양한 센서가 필요한 이유는 각 센서별로의 장단점이 있기 때문이다.
GPS
차량의 위치를 위성으로 부터 지구 좌표계의 절대 위치 정보(lat, long, altitude)를 받아온다.
1 ~ 10 HZ 의 느린 주기를 가지고 있다.
지하 / 터널 / 도심등에서 음영 지역이 발생한다.(***)
일반적인 GPS 는 수 m 오차가 있으므로, 자율주행에서는 부적합하다. (DGPS, GPS-RTK 사용)
(***) GPS 의 단점이므로 이걸 해결하기 위해서는 IMU 의 센서가 보완할수 있게 이용된다.
IMU
결국엔 차량의 위치를 절대적 좌표만으로 볼수 없기 때문에, 차량의 가속도, 각속도, 지자기 를 측정해서 자기의 위치를 판단할수 있게 한다.
200 HZ 이상의 빠른 주기 이다.
추측 항법을 통해 이동 거리 및 방향 추정 가능
외부 환경에 대해서 강건하나, 시간에 따른 누적 오차가 발생한다.
HD Maps (센서 X)
IMU 와 GPS 들의 단점을 마지막 매칭하기 위해서, 자율 주행 차량이 필요한 도로 정보를 정밀하기 기록 되어있늕 지도
도로 규칙이나 도로의 형상, 교통 정보, 센서 데이터등이 포함도니다.
Camera
개체 인지 및 위치 추정에 활용
풍부한 텍스처 정보를 포함
30 ~ 60 HZ 의 빠른주기
HD 급 카메라를 다수 장착해 FOV (Field of View) 확보.
조명 환경 변화에 취약, 정확한 깊이 추정 불가 (***)
RADAR
장애물 회피 및 충돌 감지에 활용
전자기파를 이용해 측정한 거리, 속도 정보를 제공
근거리와 원거리에 대해 모두 측정가능
눈, 비 조명 등 외부 환경에 강건
작은 물체 측정에 취약, 낮은 해상도. (4D Imaging RADAR 로 발전해 가는중)
LiDAR
개체 인지 및 위치 추정에 활용
레이저를 이용해 고해상도의 정확한 3차원 거리 정보 제공
10 ~ 20 Hz 의 주기
대상체에 대한 반사도 정보 제공
조명 환경 변화에 강건
눈, 비, 안개 등 약천후에 민감하고, 높은 비용
위와 같은 센서를 이용해서 하나의 모듈을 같이 쓰기도 하고, 다같이 쓰기도 한다. 하지만 LiDAR 에 대한 설명으로서는 아래와 같이 비교가 나와있다.
LiDAR 를 약천후가 제외한다고 하고, 많은 채널을 사용한다고 하면, 높은 해상도를 얻을수 있다고 한다. LiDAR 의 원리같은 경우는 빛의 이동 시간 (Time of Flight) 으로 측정하는 원리를 이용해서 높은 정밀도의 3차원 공간정보를 고속으로 획득할수 있다는거에 대해서 큰 장점을 가지고 있다. 대표적으로는 Velodyne 64 channel 이 있고, 이건 Scanning Lidar 인데, Scanning LiDAR 같은 경우 Motor 로 Physically 하게 돌아가게 때문에, 기계적인 요소로 인해서 Maintain 하기도 쉽지가 않아서 MEMS (Mirror Scanning LiDAR) 을 사용한다고 한다. 결국에는 빛을 Mirror 에 쏘아서 스캐닝을 한다고 한다. 물론 여기에는 단점이 있는게 FOV 이다. 그래서 여러대를 달아서 모아놓고 정합하는 작업을 후처리로 한다고 한다.
또 LiDAR 의 단점으로서는 Transmitter 에 있는데, 파장을 905 또는 1550 사이에서 적당하게 laser source 를 사용해야된다는거다. 파장을 905 를 사용하게 되면, 사람의 눈에 피해가되고, 1550 를 사용하게 되면 카메라가 파괴될수도 있다고 하기에 모든건 적절? 하게 사용해야한다라는걸 알게됬다.
Radar in Deep
RADAR (RAdio Detection And Ranging) RADAR 는 사실 학부때 들었었는데 기억나는건 RADAR 공식 밖에 기억은 안나지만 결국에는 전자기파를 송신해서, 물체에 반사된 수신신호를 통해 물체의 위치(거리, 속도, 각도)를 감지 할수 있다.
앞에서 말했던것 처럼 RADAR 의 장점으로서는 기후에대해서 굉장히 강건하다, 하지만 작은 물체에 대해서는 Detection 이 불가능하고, 이 물체가 있는지 없는지만 판별이 가능하지 이게 사람인지, 차선인지, 차량인지는 알수가 없다.
주로 사용되는 Frequency 는 77 GHZ (1Ghz Bw / 55dBm) 이고 나중에는 79 GHz(4GHz Bw, 55dBM, -3DBm/MHz) 정도 된다. 그리고 아래의 그림 처럼 현재와 미래에 대한 RADAR 가 달리는걸 보면 대체적으로 어떻게 RADAR 가 개발될지는 알수 있다.
그리고 인지쪽을 결국에 하려면, Hardware 가 Support 가 되어야하며, 그거에따른 Modulation 과 Signal Processing 이 따로 필요하다는걸 강조하셨다. 또 여기서 볼수 있는건 결국엔 Data-Driven 의 강조가 보였다.
그리고 앞서 말했듯이 각 센서들의 단점들이 존재 하지만 결국엔 어떻게 쓰는지에 따라서 였는데, 앞에서는 LiDAR 로 해결할수 있는 부분이 있다고 말씀하셨지만, 이번 bitsensing 에 계시는 분은 Camera 와 Radar 두개를 한꺼번에 융합하는 시스템이 있다고 하면, 서로 단점을 더 잘보완할거다? 라는 말씀을 하셨다.
물론 자율주행이라는게, 굉장히 상용적이여야하면서, 완성이 되려면 보수적으로 볼수 밖에 없다고 한다.
사실 다른건, 조금 Career 적으로 인것같아서 적진 않겠지만, 그래도 좋은 자료와 어떻게 자율주행쪽에서 살아가려면 어떤게 필요하고, 어떤 마음가짐으로 임해야겠다라는 생각이들었다. 그래고 Data-Driven.. 정말 rule-based 를 선호했지만, 성능차이에서는 무조건적으로 좋을수도 있지만 이게 Model 이라는게 되게 Light 해야되고, 조금 더 사용할수 있게끔 될때까지는 오래걸리기에, 조금더 넓은 안목을 가지고 자율주행쪽으로 임하는 마음을 가지게 되었다.
정말 정말 2 년동안 이런일도 많고 저런일도 많아서, 글쎄? 어떤게 제일 나에게 좋은 선물을 할수 있을까? 할수 있었던데 바로 시애틀 여행이다. 뭔가 후회도 굉장히 많이 남는게, 미국에 있었을때, 여행을 같이 갈수 있었는데도 경제적 사정도 그렇고, 뭔가 부모님에게 덜 부담을 주기 위해서 생활비를 아껴야 한다라는 그런 부담감에 사로 잡혀 많이 하지 못했던 것도 있다. 그래서 이번엔 350 만원 정도 주고 미국, 시애틀에 다시 오게 되었다. 큰 설렘과 그리고 새로운 곳에 대한 무서움도 있었다.
첫날 (11/10/2023)
첫날은 시애틀에 도착했을떄는, 참 미국이 이런곳 이였지 하면서 화장실부터 느껴졌었다. 미국 화장실의 더럽고, 약간 순박한 그런것 들이 있었는데, 또 다시 보니 은근 반가웠었다. 그리고 도착해서 담배를 피면서, 미국에 대한 그리움이 점점 더 풀려가는것 같았고, 내가 진짜 이걸 하네? 그렇게 걱정도 많았고 탈도 많았었는데 이걸 하네? 이런 생각만 들었다. 캐리어를 가지고, 셔틀 버스에 올라 타는데 영어로 이야기하는게 정말 그래웠었나? 라는 생각들 들고, 그냥 스쳐 지나가는 말들도 귀에 꽃히더라. 확실히 나는 언어를 습득 한게 맞는것 같았다. 물론 완벽하지는 않지만, 아직 많이 부족한점도 굉장히 많은것 같다. 셔틀 버스에 내려서, 차 렌트를 하는데 200+ 600 불이 나와서 생각보다 많이 나왔다고 생각했다. 하지만 뭐 인슈어런스 드는것도 나쁘지는 않으니까? 어떻게 될지 모르니까? 이러면서 그냥 지나 보냈었다. 차를 타고, 호텔까지 찍는데 37 분이나 걸리는 거리였다. 내가 가는 곳은 Microsoft 옆 Redmond 라는 동네이다. 딱히 막 비싼곳도 아니고, 그렇게 도시에서 멀지도 않으니 숙박은 정말 잘잡은것 같다.
날씨는 흐리고 비가 왔었다. 축축하지만 운전하면서 보이는, 단풍나무랑 내가 미국에서 보지 못했던 언덕들 위의 집들은 정말 다 새로웠다. 와 대박? 그러면서 또 하는 생각은 나 미국인거 맞지? 나 이렇게 이런게 그리웠을까? 이렇게 차가 많은데도 운전하는게 즐거웠었던걸까? 정말 호텔가면서 뭔가 내가 진짜 잘왔구나? 이런 생각이 들었다. 호텔에 도착하자마자, 바로 미국에서 항상 짖는 나의 미소로 check-in 하고, 바로 방으로 들어와서 짐을 풀기 시작했다. 짐풀고 씻고, 누웠더니.. 밤 11시가 되버렸다?! 그래서 내가 대학원생때 진짜 많이 먹었던 two mac chicken & midium fries 를 먹으로 갔다. 근데? 와 확실히 물가가 거기에서 확 느껴지더라 원래는 6달러에 먹을수 있었던게 10 달라가 되버린거다. 시애틀이 물가가 비싸긴 하지만, 그래도 후회없이 뇸뇸하고 오늘 하루를 잘 끝냈다.
마지막 및 소감
항상 늘 똑같이 챗바퀴처럼 돌아갔던 삶을 잠시 내려두고, 여행을 온건 정말 잘한것 같다. 그리고 독립적으로 내 스스로 많은걸 했었다. 물론 그 과정들이 외롭고 무섭기는 했어도, 그리고 돈이 조금 들더라도, 큰 용기가 생겼다. 그리고, 항상 내가 스스로 결정이 아닌 환경에 의해서 나의 결정이 났었던것 같다. 내가 스스로 뭔가를 하려고 했지만, 늘 구체적인 계획을 세워서 실패와 실망감이 더많이 들었던것 같다 라는걸 알게되었다. 구체적인 플랜은 잠시 제쳐두고, 가볍고 추상적인 계획 부터 시작해서 하나씩 하나씩 붙여나가는걸 해야겠다라는 생각이 무척들었다. 물론 이 여정가운데서, 무서워서 운전하다가 손에 땀이 나고 그랬던 적이 여러번이였지만, 그만큼의 두려움은 결국 행복과 목표에 도달했을때의 성취감이 들더라. 후회는 남기지 않는 삶을 내가 만들어가야하겠다 라는 생각도 정말 많이 들었다. 감정을 잠시 제쳐두고, 내가 진짜 뭘하고 싶은지, 어떤 생각으로 내가 이런 저런것을 할수 있는지 부터 차근차근 살을 붙이는 작업을 해야겠더라. 미국의 삶 물론 외로운점도 굉장히 많고, 혼자 해내야할 산들이 정말 많았구나, 고생많았구나 라는 점도 많이 느끼고, 한국이랑 정말 다른점을 느끼게 되더라. 어느새는 내가 정말 한국인인가 보다 라는걸, 성급해서 맥도널드에 직접 찾아가서 버거 달라고 하는걸 보면 많이 한국 스러워졌다? 라고 말을 할수 있게되더라.
미국에 가는건 더 clearer 해졌다. 뭔가 나의 행복과 삶을 찾기 위해서는 이게 방법이다 라는걸 쌓게 되는 계기가 되고, 그리고 목표도 조금 뚜렸해졌다. 이제는 사람에 이끌어지지말고, 조금 필요한곳에 자존심을 세워서 자신감있게 해내야겠다라ㅏ는걸 생각 하게 되었다. 물론 지금 현재는 부족한게 많을수 밖에 없지만, 그래도 이게 하나의 계기가 되서 좋은 기억과 추억들이 나의 하나의 Journey 가 될수 있게 하는 계기가 되서 좋았다. Having a freedom in my life is supposed to be getting out of comfort zone 이 맞는 말이라는걸 알게 되서 정말 좋은것 같다. 여행을 시작하기전과 여행을 끝내고 나서의 나의 모습들이 좋게 달라졌으면 하는 바램으로, 이번의 시애틀 여행을 잘 맞춘다.
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 을 한다.
너무 비싸다른것(lowering the price per unit)
Decreasing package size
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 하게 잘싸옇져있다.
Long Ranging Distance
Wide Horizontal FOV
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 를 사용하면 된다.
결국엔 이 Dataset 을 하기 위해선, Point Cloud Data 로 가지고 와야하지만, 여기에서 Point Cloud Data 이외에 표현하고 Visualize 를 하기 위해서는, 위의 Range Map 을 사용하면 된다. 여기에 Waymo Dataset 에서 한 Frame 당 구하기 위해선, 하나의 Frame 을 Matrix 로 변환이후에 reshape 을 해주면 shape (64, 2650, 4) 가 나온다. 아래의 코드는 Top Lidar 를 가지고 와서 Dimension 을 확인할수 있다.
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 의 채널로 나눠준 각도를 구해주는 것이다.
Range Image 이의 Range 의 Value 값들은, 환경속의 특정 포인트까지의 거리를 2D 이미지로 담아냈기때문에, 센서부터 거리(distance) 를 말한다. 근데 여기에서 min = -1 일때가 있는데 geometrically 하게 make sense 하지 않는다. 그래서 Filter 를 한번 해줘야한다. 자세한 내용은 Waymo Dataset Paper 을 참고하자. 일단 이부분을 구현한 부분은 아래와 같다.
defload_range_image(frame,lidar_name):lidar=[objforobjinframe.lasersifobj.name==lidar_name][0]ri=[]iflen(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)returnridefget_max_min_ranage(frame,lidar_name):ri=load_range_image(frame,lidar_name)# ri[:, :, 0] -> range
# ri[:, :, 1] -> intensity
ri[ri<0]=0.0print('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 의 결과의 이미지가 이싿.
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 이라고 한다.
그래서 위의 내용을 적용하면 아래와 같이 사진이나오는데, 차량의 licence plate 가 reflective 하기 때문에 차량의 뒷편에 intensity 가 높은걸 확인할수있다.
defvisualize_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 로 변경해야한다.
일단 Range Image 로 부터 Point Cloud Data 를 가지고오려면, 위에 했던 내용을 결국은 사용해야하며, Calibration Data 를 Waymo Dataset 에서 가져와야한다. 그리고 결국엔 vehicle 이 x axis 로 보게끔 range image 를 correction 을 거쳐야한다. 이때, extrinsic calibration matrix 를 가지고 와야한다.
아래는 Python range_image 를 Point Cloud 변경하는 코드이다.
calibration 을 하기 위해서, calibration data 를 가지고 온다.
그 data 에서 extrinsic matrix 를 가지고 와서, azimuth 를 구해준다. 이때 값을 구할때, [1, 0] 과 xetrinsic[0, 0], spherical coordinates 에서 world coordinate 으로 변경한 Y 와 X 의 값이다.
실제 고쳐야되는 azimuth 가 있었다면 -180 부터의 180 까지에서 corrected 된걸 연산해준다.
Corrected 된걸 가지고, World Coordinates X, Y, Z 를 연산해줘서, 센서의 위치를 파악한다.
Sensor 위치가 나오면, extrinxisc 과 xyz_sensor 를 matrix multiplication 을 통해서 ego coordinate system 으로 변경한다.
Point Cloud Data 를 (64, 2560, 4) 변경시켜서, 일단 거리가 0 보다 작은것들은 row 로 masking 을 시켜서, filtering 을 한이후 0:3 까지의 값들은 가지고 온다.(3 의값은 1 –> Homogeneous Coordinates)
Point Cloud Data 를 그린다.
defrange_image_to_point_cloud(frame,lidar_name):ri=load_range_image(frame,lidar_name)ri[ri<0]=0.0ri_range=ri[:,:,0]# load calibration data
calibration=[objforobjinframe.context.laser_calibrationsifobj.name=lidar_name][0]# compute vertical beam inclination
height=ri_range.shape[0]inclination_min=calibration.beam_inclination_mininclination_max=calibration.beam_inclination_maxinclination=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_correctedazimuth_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_rangey=np.sin(azimuth_tiled)*np.cos(incliation_tiled)*ri_rangez=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>0pcl=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]))returnpcl_full
일단, Texturing 을 하기 위해서, Shader Programming 해야된다. 일단 Texturing 을 하려면 Vertex Shader 에서 해야되는지, Pixel Shader 에서 해야되는지가 고민이 되는데, 일단 Microsoft 공식문서에서는 Pixel Shader 에서 하라고 명시가 되어있다. 어떤 특유의 Texture 를 결국에는 Vertex 의 Point 에 맞게끔, Image 를 Texture Coordinate 으로 Mapping 을 해줘야한다.
CPU 에서의 Texture 를 사용하기위한 Resource 를 먼저 만들어보자. 아래처럼 Texture2D 를 만들어주고, 그리고 ID3D11ShaderResourceView 는 Texture 를 Shader 에서 사용할수 있게끔, ResourceView 를 Mapping 해줬다고 볼수 있다. 그리고 Texture 를 Sampling 하는 SamplerState 도 정해놓을걸 볼수 있다.
정리하자면 Shader 를 Resource 로 사용하는 View 로 보겠다가 되겠고, Texture 를 RenderTarget 으로도 사용할수 있다. (RenderTargetView 참고), 또 이렇게 저렇게 Texture 로 나온 결과값을 다른 Shader 에서도 집어 넣는게 가능하다.
그리고 이러한 Texture 을 Rendering Stage 에서, Setter 를 맞춰줘야한다. 여러개의 Texture Resource 를 사용할수 있으니, 아래처럼 처리할수 있다.
그리고 만약 Texture 를 초기화 할떄 한다고 한다면, Sample Description 을 Init 을 해야한다. 옵션은 여러개 있지만, 예를 들어서 D3D11_TEXTURE_ADDRESS_WRAP / D3D11_TEXTURE_ADDRESS_MIRROR / D3D11_TEXTURE_ADDRESS_CLAMP 등이 있다.
그러면 PixelShader 의 Program 을 잠깐 봐보자. 일단 GPU 에서 Texture Image 를 Texture2D 로 받아 올수 있다. 그리고 앞에서 잠깐 언급했듯이, Texture Image 안에서 색깔 값을 가져오는 걸 Sampling 이라고 했었다. 그래서 SamplerState 도 사용해야한다. 여기서 register 일때의 t0 과 s0 이 있다. t0 같은 경우, Texture 일때, 그리고 index 는 0, 그리고 sampler 일때는 s 그리고 index 는 0. 자세한건, 이 Resource 를 활용하자.
만약 다른 옵션으로 정의 한다고 했을때, Clamp, Wrap, Mirror 를 한번씩 보자. 일단 더 정확하게 보려면, Shader 코드의 변경점이 필요하다.
return g_texture0.Sample(g_sampler, input.textcoord * 5); 이런식으로 Clamp 부터 확인 해보자.
Clamp 같은 경우는 약간 가장자리를 쭈욱 댕기는 느낌이라고 볼수있다. 작은 Texture 를 잡아당기기때문에 ZoomIn/ZoomOut 느낌은 아니다.
그 다음에 Wrap 같은 경우는 5 x 5 의 작은 Texture 이 Mapping 이된걸 볼수 있다.
그 다음에 Mirror 같은 경우는 5 x 5 인데 마치 데칼코마니 처럼 보여질수 있는걸 볼수 있다.
다른 Image (png, jpeg) 의 변환작업이 필요하지만, 이건 ResourceView 로 Mapping 만 잘해주면 될것 같다.
주의해야될점은 한 Texture 를 한 Shader 안에서 동시에 Resource 와 RenderTarget 으로 사용할수 없다. 그래서 Texture 가 두개가 있다고, 가정하면, Res1 -> Shader -> RT2 = Res2 -> Shader -> RT1 이렇게 사용이 가능하다.
가끔씩 나는 미국이 그리울때가 많다. 남들이 한국어를 잘한다고 생각하지만, 나는 한국어가 어려운 언어중에 하나라고 생각한다. 그리고, 나는 그냥 영어를 듣기를 좋아하고 말하는것도 한국어에비해 더 자유로움을 많이 느낀다. 그래서 오랜만에 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 이다.
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 할수 있다.
위의 코드 같은 경우 float3 pos : POSITION 이라고 나와있는데 colon(:) 다음에 나오는건 Sematics 인데, 어떤 Parameter 종류다라는것을 명시한다. 자세한건 Shader Semantics 참고하자.
그렇다면 Vertex Shader 와 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 등 여러가지 타입이 존재한다.
정리를 하자면, 어떤 Stage 간에, PixelShader / Vertex Shader 에 Input 으로 들어올수 있는 16 byte 로 fit 하게 들어가야하며, ConstantBuffer 같은 경우, 어떤 Shader 에서 연결을 해줘서 Resource 를 Binding 을 해주면 된다.
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 InterfaceMicrosoft::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 라고 생각하면된다. 그래서 생성될때, ID3D11Device 와 ID3D11DeviceContext interface 를 통해서 생성된다.
일단 Device 에 관련되서 생성을 하려면 아래와 같이 해야한다. 그리고 필요한건 context 이다.
Microsoft::WRL::ComPtr<ID3D11Device>device;// COM InterfaceMicrosoft::WRL::ComPtr<ID3D11DeviceContext>context;constD3D_FEATURE_LEVELfeatureLevels[2]={D3D_FEATURE_LEVEL_11_0,D3D_FEATURE_LEVEL_9_3};D3D_FEATURE_LEVELfeatureLevel;HRESULThr=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 Buffer 는 MVP (Model, View, Projection) 으로 정의 되어있다. Constant Buffer 의 의미상으로는 어떤 static data 를 가지고 있는데, 이게 pixel shader 호출에 필요한 정적데이터를 또는 모든 버텍스 및 Pixel 를 가르친다. 즉 Buffer 들을 CPU 가 정의했다가, GPU 에 넘기는 용도로 사용된다.
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 라고 볼수 있다.
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 vec3RotateAboutZ(constvec3&v,constfloat&theta){returnvec3(v.x*cos(theta)-v.y*sin(theta),v.x*sin(theta)+v.y*cos(theta),v.z);}vec3RotateAboutY(constvec3&v,constfloat&theta){returnvec3(v.x*cos(theta)+v.z*sin(theta),v.y,-v.x*sin(theta)+v.z*cos(theta));}vec3RotateAboutX(constvec3&v,constfloat&theta){returnvec3(v.x,v.y*cos(theta)-v.z*sin(theta),v.y*sin(theta)+v.z*cos(theta));}structTransformation{vec3scale=vec3(1.0f);vec3translation=vec3(0.0f);floatrotationX=0.0f;floatrotationY=0.0f;floatrotationZ=0.0f;};structMaterial{vec3ambient=vec3(0.1f);vec3diffuse=vec3(1.0f);vec3specular=vec3(1.0f);floatshininess;};sturctLight{vec3strength=vec3(1.0f);vec3direction=vec3(0.0f,-1.0f,0.0f);};structConstants{Transformationtransformation;Lightlight;Materialmaterial;}constants;structVSInput{vec3position;vec3normal;vec3color;};structVSOutput{vec3position;vec3normal;};vec3BlinnPhong(vec3lightStrength,vec3lightVec,vec3normal,vec3toEye,Materialmat){vec3halfway=normalize(toEye+lightVec);vec3specular=mat.specular*pow(glm::max(dot(halfway,normal),0.0f),mat.shininess);returnmat.ambient+(mat.diffuse+specular)*lightStrength;}vec3ComputeDirectionalLight(LightL,Materialmat,vec3normal,vec3toEye){vec3lightVec=-L.direction;floatndotl=glm::max(dot(lightVec,normal),0.0f);// exception for negative valuevec3lightStrength=L.strength*ndotl;returnBlinnPhong(lightStrength,lightVec,normal,toEye,mat);}// Vertex ShaderVSOutputMyVertexShader(constVSInputvsInput){VSOutputvsOutput;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);returnvsOutput;// Pixel Shader structPSInput{vec3position;vec3normal;vec3color;vec2uv;}vec4MyPixelShader(constPSInputpsInput){vec3eye=vec3(0.0f,0.0f,-1.0f);vec3toEye=glm::normalize(eye-psInput.position);vec3color=ComputeDirectionalLight(constants.light,constants.material,psInput.normal,toEye);returnvec4(color,1.0f);}}
이런식으로 구현이 가능하다. 여기서 Minor 한점은 glm::max(…) 를 통해서 각이 90 도 이상 일때 빛의 세기가 덜받으면 덜받았지 음수가 될수 없다는 점과. Blinn-phong algorithm 을 사용해서 halfway vector 의 방향을 구한다음에 color 값을 return 해주는 함수가 있다는 점을 한번 참고하자.
Ray Tracing 으로 Rendering 을 하게 되면, 여러 Bottleneck 이 존재한다. 일단 모든 물체에 대해서 물체가 부딫혀서 색깔을 가져와야하며, 물체의 Material 이 Transparncy 나 Reflection 이 있다면 반사 또는 굴절(reflection ray & refracted ray) 를 만들어서 다시 쏴주는것도 한계가 있으며, 또 물체의 그림자를 표현하려면, shadow ray 를 만들어서 물체의 충돌을 계산해야한다. 그래서 실시간으로 Rendering 을 하기에는 Computation Expensive 하다. 물론, 다른 Data Structure 를 사용한다면, 분명 최적화가 가능하지만, 일단 이러한 점을 CPU 에서 해결하는게 효율적이지 못하다.
이걸 해결하기위한 방법이 Rasterization Algorithm 이다. 일단 Approach 자체가 다르다.
Rasterization Algorithm
일단 앞서서, Ray Tracing 을 사용할때, 역방향으로 Ray 를 쏴줬다. 더 자세하게 설명을 하면, 우리가 보는 위치(눈) 에서 Ray 를 Screen 으로 100 개를쏴서, 그 100 개가 물체에 충돌하는지 충돌 안하는지를 계산을 했었다. 하지만, Rasterization 에서는 물체의 형상을 Screen 좌표계로 투영을 시킨다는 점이다. 즉 정점들을 Screen 좌표로 투영을 시켜서, 삼각형이라고 한다면, 그 3 개의 Vertex 를 정점을 투영시켜, 정점들을 연결시켜서 Render 또는 그리면 된다. 그래서 Screen 좌표 Block 에 삼각형이 들어있는지 없는지만 체크하면된다. 마치 충돌처럼 보일수는 있어도, Pixel 이 있는지 없는지만 확인을 하면 되기때문에, 더 빠르다. 아래의 그림을 보면 조금더 이해가 잘될거다.
Rasterization 의 알고리즘의 Step 은 Ray Tracing 과 비슷하면서도 다르다. 예를 들어서 Ray Tracing 같은 경우, 모든 Pixel 에 대해서 Loop 을 돌면서 모든 물체에 Hit 을 하는지 안하는지를 체크 한이후에, 물체에 부딫히면 물체의 색을 결정했었다. Rasterization 같은 경우, 기준이 scene 에있는 모든 물체에서 Loop 을 돌고, 그 이후에 모든 물체의 Vertex 들을 투영을 시킨다음에, 그다음에 Pixel 을 돌면서, 그 Pixel 이 물체안에 들어가있는지 없는지를 체크를 한이후에 들어가 있다면, color 값을 가지고 오면 된다. 여기서 중요한건 Rasterization Algorithm 은 object-centric 이라는 점이다. 즉 도형의 Geometry 를 image 좌표계로 바꿔서, 그 Image 를 Loop 을 돌기 때문이다.
즉, 가상공간에 있는 삼각형을 가지고, Screen 좌표계로 투영을 시킨 이후에, pixel 마다 체크 하면서, 삼각형 밖에 있는 Pixel 인가, 안에 있는 Pixel 인가 체크 하면서, 들어있을때는 Screen 에다가 색깔을 칠해주고, 아니면 색깔을 안칠하면 되는 식이다. 근데 모든 Pixel 을 돌게 되면 되게 비효율적이다. 그래서 가장 작은 Bounding Box 를 그려서 효율성을 높인다.
Rasterization Prep
Baycentric Coordinates 여기에서 알아볼 점은, 삼각형 정점 PQR 이 존재한다고 했을때 그 안에 T 를 Affine Sum 으로 표현하는게 중요하다.
위의 그림과 같이 R 의 좌표와 T 의 좌표를 이미 알기때문에, R 과 T 를 포함하는 직선을 그려서, PQ 직선의 점 S 에 그리게 되면 P 와 S 의 거리, S 와 Q 의 거리의 비를 찾을수 있다. 그러면 S 의 위치를 S = P + (1 - alpha)(Q - P) 라는 식을 구할수있다.
그 이후에, 아래와 같이 T 의 위치를 PQ 에서 S 의 위치를 구하듯이, Beta 를 이용해서 구할수 있다. 그래서 T = beta * S + (1 - beta)*R 이런식으로 구한다음에 S 의 값을 위의 식에 대입을 하면 T 의 좌표를 Affine Combination 을 구할수 있다. T = alpha * beta * P + beta(1 - alpha)*Q + (1 - beta)*R 이런식으로 표현이 된다.
Rasterization Implementation (How to draw Triangle)
언제나 삼각형 그리는게 기본중에 기본이다. 일단 Rasterization 어떻게 구현해야될지 class 구조를 짜보자. 일단 Header 파일에서 Rasterization 의 생성자를 만들때, with 와 height 의 정보를 가지고 오며, ProjectWorldToRaster 같은 경우는 World 좌표계에 정의된 정점들을 Screen 좌표계로 이동시키면 되는 함수가 있으며, Util Function 으로는 Edge Function 이 있다. 그리고 Render 와 매 Frame 마다 Update 를 하는 Update 함수가 있다.
usingnamespaceglm;usingnamespacestd;structVertex{vec3pos;vec3color;};structTriangle{Vertexv0,v1,v2;};classRasterization{public:Rasterization(constint&width,constint&height)vec2ProjectWorldToRaster(vec3point);floatEdgeFunction(constvec2&v0,constvec2&v1,constvec3&point);voidRender(vector<vec4>&pixels);voidUpdate();public:intwidth;intheight;Triangletriangle;};voidRasterization::Rasterization(constint&width,constint&height):width(width),height(height){triangle.v0.pos={0.0,0.5,1.0f};triangle.v1.pos={1.0,-0.5,1.0f};triangle.v2.pos={-1.0,-0.5,1.0f};triangle.v0.color={1.0f,0.0f,0.0f};// Redtriangle.v1.color={0.0f,1.0f,0.0f};// Greentriangle.v2.color={0.0f,0.0f,1.0f};// Blue}voidRasterization::Render(vector<vec4>&pixels){// Compute World Coordinates to Screen Coordinatesconstautov0=ProjectWorldToRaster(triangle.v0.pos);constautov1=ProjectWorldToRaster(triangle.v1.pos);constautov2=ProjectWorldToRaster(triangle.v2.pos);// Find the bounding boxconstautoxMin=size_t(glm::clamp(glm::floor(std::min({v0.x,v1.x,v2.x})),0.0f,float(width-1)));constautoyMin=size_t(glm::clamp(glm::floor(std::min({v0.y,v1.y,v2.y})),0.0f,float(height-1)));constautoxMax=size_t(glm::clamp(glm::ceil(std::max({v0.x,v1.x,v2.x})),0.0f,float(width-1)));constautoyMax=size_t(glm::clamp(glm::ceil(std::max({v0.y,v1.y,v2.y})),0.0f,float(height-1)));for(size_tj=yMin;j<=yMax;j++){for(size_ti=xMin;i<=xMax;i++){// Check if the pixel is inside of triangle// Get the pixel info// A Parallel Algorithm for Polygon Rasterizationconstvec2point=vec2(float(i),float(j));constfloatalpha0=EdgeFunction(v1,v2,point);constfloatalpha1=EdgeFunction(v2,v0,point);constfloatalpha2=EdgeFunction(v0,v1,point);if(alpha0>=0.0f&&alpha1>=0.0f&&alpha2>=0.0f){constfloatarea=alpha0+alpha1+alpha2;constfloatw0=alpha0/area;constfloatw1=alpha1/area;constfloatw2=alpha2/area;constvec3color=(w0*triangle.v0.color+w1*triangle.v1.color+w2*triangle.v2.color);pixels[i+width*j]=vec4(color,1.0f);}}}}vec2Rasterization::ProjectWorldToRaster(vec3point){// ** Orthographics Projection ** //// Convert to NDC(Normalized Device Coordinates)// NDC Range [-1, 1] x [-1, 1]constfloataspect=float(width)/height;constvec2pointNDC=vec2(point.x/aspect,point.y)// Rasterization Coordinates Range: [-0.5, width -1 + 0.5] x [-0.5, height - 1 + 0.5]constfloatxScale=2.0f/width;constfloatyScale=2.0f/height;// NDC -> Rasterizationreturnvec2((pointNDC.x+1.0f)/xScale-0.5f,(1.0f-pointNDC.y)/yScale-0.5f);}floatRasterization::EdgeFunction(constvec2&v0,constvec2&v1,constvec2&point){constvec2a=v1-v0;constvec2b=point-v0;return(a.x*b.y-a.y*b.x)*0.5;}
여기서 중요한건 위와 같이 Render 를 할때, 각 정점을 Orthographics Projection 해주는 함수 ProjectWorldToRaster 를 통해서, Screen 좌표계로 옮겨주고, Bounding Box 를 찾을수 있게 해준다음에, Bounding Box 안에서 Edge Function 알 사용해서, Pixel 이 삼각형 안에 들어가져있는지 없는지 확인을 한 이후에, Barycentric Coordinates 을 사용해서 Pixel 값을 정해주면 된다. 참고: Edge Function 같은 경우, Pixel 이 삼각형안에 들어왔는지 없는지를 확인 하는 함수이다. 아래의 그림을 참고하자.