Tóm tắt
6 nghệ sĩ được mời vẽ, thiết kế và điêu khắc trong VR. Đây là quy trình chúng tôi ghi lại các phiên hoạt động của họ, chuyển đổi dữ liệu và trình bày dữ liệu theo thời gian thực bằng trình duyệt web.
https://g.co/VirtualArtSessions
Thật là một thời điểm tuyệt vời để sống! Khi thực tế ảo được giới thiệu dưới dạng một sản phẩm tiêu dùng, các khả năng mới và chưa được khám phá đang được phát hiện. Tilt Brush, một sản phẩm của Google có trên HTC Vive, cho phép bạn vẽ trong không gian ba chiều. Khi chúng tôi dùng thử Tilt Brush lần đầu tiên, cảm giác vẽ bằng tay điều khiển theo dõi chuyển động cùng với cảm giác "ở trong một căn phòng có siêu năng lực" sẽ còn đọng lại trong bạn; thực sự không có trải nghiệm nào giống như việc có thể vẽ trong không gian trống xung quanh bạn.
Nhóm Nghệ thuật dữ liệu tại Google đã gặp phải thách thức khi muốn giới thiệu trải nghiệm này cho những người không có thiết bị đeo thực tế ảo, trên web nơi Tilt Brush chưa hoạt động. Để làm được điều đó, nhóm đã mời một nhà điêu khắc, một họa sĩ minh hoạ, một nhà thiết kế ý tưởng, một nghệ sĩ thời trang, một nghệ sĩ sắp đặt và các nghệ sĩ đường phố để tạo ra tác phẩm nghệ thuật theo phong cách riêng của họ trong phương tiện mới này.
Ghi lại bản vẽ trong thực tế ảo
Được tích hợp trong Unity, bản thân phần mềm Tilt Brush là một ứng dụng dành cho máy tính sử dụng công nghệ thực tế ảo theo tỷ lệ phòng để theo dõi vị trí đầu của bạn (màn hình gắn trên đầu hoặc HMD) và các tay điều khiển ở mỗi tay. Theo mặc định, tác phẩm nghệ thuật được tạo trong Tilt Brush sẽ được xuất dưới dạng tệp .tilt
. Để mang trải nghiệm này lên web, chúng tôi nhận thấy mình cần nhiều dữ liệu hơn là chỉ dữ liệu hình minh hoạ. Chúng tôi đã làm việc chặt chẽ với nhóm Tilt Brush để sửa đổi Tilt Brush sao cho ứng dụng này xuất các thao tác huỷ/xoá cũng như vị trí đầu và tay của nghệ sĩ với tốc độ 90 lần/giây.
Khi vẽ, Tilt Brush sẽ lấy vị trí và góc của tay điều khiển, đồng thời chuyển đổi nhiều điểm theo thời gian thành một "nét vẽ". Bạn có thể xem ví dụ tại đây. Chúng tôi đã viết các trình bổ trợ trích xuất các nét vẽ này và xuất dưới dạng JSON thô.
{
"metadata": {
"BrushIndex": [
"d229d335-c334-495a-a801-660ac8a87360"
]
},
"actions": [
{
"type": "STROKE",
"time": 12854,
"data": {
"id": 0,
"brush": 0,
"b_size": 0.081906750798225,
"color": [
0.69848710298538,
0.39136275649071,
0.211316883564
],
"points": [
[
{
"t": 12854,
"p": 0.25791856646538,
"pos": [
[
1.9832634925842,
17.915264129639,
8.6014995574951
],
[
-0.32014992833138,
0.82291424274445,
-0.41208130121231,
-0.22473378479481
]
]
}, ...many more points
]
]
}
}, ... many more actions
]
}
Đoạn mã trên trình bày định dạng của tệp JSON phác thảo.
Tại đây, mỗi nét vẽ được lưu dưới dạng một hành động, với loại: "STROKE". Ngoài các thao tác vẽ nét, chúng tôi muốn cho thấy một nghệ sĩ mắc lỗi và thay đổi ý định giữa chừng khi phác thảo. Vì vậy, điều quan trọng là phải lưu các thao tác "XOÁ" để xoá hoặc huỷ thao tác cho toàn bộ nét vẽ.
Thông tin cơ bản cho mỗi nét vẽ được lưu, vì vậy, loại bút vẽ, kích thước bút vẽ, màu RGB đều được thu thập.
Cuối cùng, mỗi đỉnh của nét vẽ được lưu và bao gồm vị trí, góc, thời gian cũng như cường độ áp lực của cò điều khiển (được ghi là p
trong mỗi điểm).
Lưu ý rằng phép xoay là một quaternion 4 thành phần. Điều này rất quan trọng sau này khi chúng ta kết xuất các nét vẽ để tránh tình trạng khoá gimbal.
Phát lại bản phác thảo bằng WebGL
Để hiển thị bản phác thảo trong trình duyệt web, chúng tôi đã sử dụng THREE.js và viết mã tạo hình học mô phỏng những gì Tilt Brush thực hiện.
Mặc dù Tilt Brush tạo ra các dải tam giác theo thời gian thực dựa trên chuyển động của tay người dùng, nhưng toàn bộ bản phác thảo đã "hoàn tất" vào thời điểm chúng tôi hiển thị bản phác thảo đó trên web. Điều này cho phép chúng ta bỏ qua phần lớn phép tính theo thời gian thực và kết xuất hình học khi tải.
Mỗi cặp đỉnh trong một nét vẽ tạo ra một vectơ hướng (các đường màu xanh dương kết nối từng điểm như minh hoạ ở trên, moveVector
trong đoạn mã dưới đây).
Mỗi điểm cũng chứa một hướng, một quaternion đại diện cho góc hiện tại của tay điều khiển. Để tạo một dải tam giác, chúng ta lặp lại trên từng điểm này để tạo các pháp tuyến vuông góc với hướng và hướng của tay điều khiển.
Quy trình tính toán dải tam giác cho mỗi nét vẽ gần giống với mã dùng trong Tilt Brush:
const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );
function computeSurfaceFrame( previousRight, moveVector, orientation ){
const pointerF = V_FORWARD.clone().applyQuaternion( orientation );
const pointerU = V_UP.clone().applyQuaternion( orientation );
const crossF = pointerF.clone().cross( moveVector );
const crossU = pointerU.clone().cross( moveVector );
const right1 = inDirectionOf( previousRight, crossF );
const right2 = inDirectionOf( previousRight, crossU );
right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );
const newRight = ( right1.clone().add( right2 ) ).normalize();
const normal = moveVector.clone().cross( newRight );
return { newRight, normal };
}
function inDirectionOf( desired, v ){
return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}
Việc kết hợp hướng và hướng nét vẽ riêng lẻ sẽ trả về kết quả không rõ ràng về mặt toán học; có thể có nhiều pháp tuyến được lấy và thường sẽ tạo ra một "vặn xoắn" trong hình học.
Khi lặp lại các điểm của một nét vẽ, chúng ta duy trì một vectơ "phải ưu tiên" và truyền vectơ này vào hàm computeSurfaceFrame()
. Hàm này cung cấp cho chúng ta một pháp tuyến để có thể lấy một hình tứ giác trong dải hình tứ giác, dựa trên hướng của nét vẽ (từ điểm cuối đến điểm hiện tại) và hướng của bộ điều khiển (một quaternion). Quan trọng hơn, hàm này cũng trả về một vectơ "phải ưu tiên" mới cho tập hợp phép tính tiếp theo.
Sau khi tạo hình tứ giác dựa trên các điểm điều khiển của mỗi nét vẽ, chúng ta hợp nhất các hình tứ giác bằng cách nội suy các góc của chúng, từ hình tứ giác này sang hình tứ giác khác.
function fuseQuads( lastVerts, nextVerts) {
const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );
lastVerts[1].copy( vTopPos );
lastVerts[4].copy( vTopPos );
lastVerts[5].copy( vBottomPos );
nextVerts[0].copy( vTopPos );
nextVerts[2].copy( vBottomPos );
nextVerts[3].copy( vBottomPos );
}
Mỗi hình tứ giác cũng chứa các UV được tạo ở bước tiếp theo. Một số bút vẽ chứa nhiều mẫu nét vẽ để tạo cảm giác rằng mỗi nét vẽ đều giống như một nét vẽ khác của bút vẽ. Điều này được thực hiện bằng cách sử dụng _bản đồ hoạ tiết_, trong đó mỗi hoạ tiết của bút vẽ chứa tất cả các biến thể có thể có. Chọn đúng hoạ tiết bằng cách sửa đổi các giá trị UV của nét vẽ.
function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
let fYStart = 0.0;
let fYEnd = 1.0;
if( useAtlas ){
const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
fYStart = fYWidth * atlasIndex;
fYEnd = fYWidth * (atlasIndex + 1.0);
}
//get length of current segment
const totalLength = quadLengths.reduce( function( total, length ){
return total + length;
}, 0 );
//then, run back through the last segment and update our UVs
let currentLength = 0.0;
quadUVs.forEach( function( uvs, index ){
const segmentLength = quadLengths[ index ];
const fXStart = currentLength / totalLength;
const fXEnd = ( currentLength + segmentLength ) / totalLength;
currentLength += segmentLength;
uvs[ 0 ].set( fXStart, fYStart );
uvs[ 1 ].set( fXEnd, fYStart );
uvs[ 2 ].set( fXStart, fYEnd );
uvs[ 3 ].set( fXStart, fYEnd );
uvs[ 4 ].set( fXEnd, fYStart );
uvs[ 5 ].set( fXEnd, fYEnd );
});
}
Vì mỗi bản phác thảo có số nét vẽ không giới hạn và các nét vẽ sẽ không cần được sửa đổi trong thời gian chạy, nên chúng ta sẽ tính toán trước hình học nét vẽ và hợp nhất các nét vẽ đó thành một lưới duy nhất. Mặc dù mỗi loại bút vẽ mới phải là một chất liệu riêng, nhưng điều đó vẫn làm giảm số lệnh gọi vẽ xuống còn một lệnh gọi cho mỗi bút vẽ.
Để kiểm thử nghiêm ngặt hệ thống, chúng tôi đã tạo một bản phác thảo mất 20 phút để lấp đầy không gian bằng nhiều đỉnh nhất có thể. Bản phác thảo thu được vẫn phát ở tốc độ 60 khung hình/giây trong WebGL.
Vì mỗi đỉnh ban đầu của một nét vẽ cũng chứa thời gian, nên chúng ta có thể dễ dàng phát lại dữ liệu. Việc tính toán lại các nét vẽ trên mỗi khung hình sẽ rất chậm, vì vậy, chúng tôi đã tính toán trước toàn bộ bản phác thảo khi tải và chỉ hiển thị từng hình tứ giác khi đến thời điểm đó.
Việc ẩn một hình tứ giác chỉ đơn giản là thu gọn các đỉnh của hình tứ giác đó về điểm 0,0,0. Khi thời gian đạt đến điểm mà hình tứ giác dự kiến sẽ hiển thị, chúng ta sẽ định vị lại các đỉnh về vị trí ban đầu.
Một khía cạnh cần cải thiện là thao tác hoàn toàn các đỉnh trên GPU bằng chương trình đổ bóng. Cách triển khai hiện tại đặt các đỉnh này bằng cách lặp lại qua mảng đỉnh từ dấu thời gian hiện tại, kiểm tra xem đỉnh nào cần được hiển thị rồi cập nhật hình học. Điều này gây ra nhiều tải cho CPU, khiến quạt quay cũng như lãng phí thời lượng pin.
Ghi âm nghệ sĩ
Chúng tôi cảm thấy bản phác thảo đó là chưa đủ. Chúng tôi muốn cho thấy các nghệ sĩ bên trong bản phác thảo, vẽ từng nét vẽ.
Để chụp các nghệ sĩ, chúng tôi đã sử dụng máy ảnh Microsoft Kinect để ghi lại dữ liệu chiều sâu của cơ thể nghệ sĩ trong không gian. Điều này cho phép chúng ta hiển thị các hình ba chiều của chúng trong cùng không gian mà bản vẽ xuất hiện.
Vì cơ thể của nghệ sĩ sẽ che khuất khiến chúng ta không nhìn thấy những gì ở phía sau, nên chúng tôi đã sử dụng hệ thống Kinect đôi, cả hai đều ở hai bên phòng, hướng về giữa.
Ngoài thông tin về độ sâu, chúng tôi cũng chụp thông tin về màu sắc của cảnh bằng máy ảnh DSLR tiêu chuẩn. Chúng tôi đã sử dụng phần mềm DepthKit tuyệt vời để hiệu chỉnh và hợp nhất cảnh quay từ máy ảnh độ sâu và máy ảnh màu. Kinect có thể quay video có màu, nhưng chúng tôi chọn sử dụng máy ảnh DSLR vì có thể kiểm soát chế độ cài đặt phơi sáng, sử dụng ống kính cao cấp đẹp mắt và quay video ở độ phân giải cao.
Để quay cảnh này, chúng tôi đã xây dựng một phòng đặc biệt để đặt HTC Vive, nghệ sĩ và máy quay. Tất cả các bề mặt đều được phủ bằng vật liệu hấp thụ ánh sáng hồng ngoại để tạo ra đám mây điểm rõ ràng hơn (vải nhung trên tường, thảm cao su có gân trên sàn). Trong trường hợp vật liệu xuất hiện trong cảnh quay của đám mây điểm, chúng tôi đã chọn vật liệu màu đen để không gây mất tập trung như khi vật liệu có màu trắng.
Các bản ghi video thu được cung cấp cho chúng tôi đủ thông tin để chiếu một hệ thống hạt. Chúng tôi đã viết một số công cụ bổ sung trong openFrameworks để làm sạch thêm cảnh quay, cụ thể là xoá sàn, tường và trần nhà.
Ngoài việc hiển thị các nghệ sĩ, chúng tôi cũng muốn kết xuất HMD và tay điều khiển ở định dạng 3D. Điều này không chỉ quan trọng để hiển thị rõ HMD trong kết quả cuối cùng (ống kính phản chiếu của HTC Vive đã làm sai lệch kết quả đọc hồng ngoại của Kinect), mà còn giúp chúng tôi có điểm tiếp xúc để gỡ lỗi đầu ra hạt và căn chỉnh video với bản phác thảo.
Điều này được thực hiện bằng cách viết một trình bổ trợ tuỳ chỉnh vào Tilt Brush để trích xuất vị trí của HMD và tay điều khiển trong mỗi khung hình. Vì Tilt Brush chạy ở tốc độ 90 khung hình/giây, nên hàng tấn dữ liệu đã được truyền ra và dữ liệu đầu vào của bản phác thảo có kích thước trên 20 MB chưa nén. Chúng tôi cũng sử dụng kỹ thuật này để ghi lại các sự kiện không được ghi lại trong tệp lưu thông thường của Tilt Brush, chẳng hạn như khi nghệ sĩ chọn một tuỳ chọn trên bảng công cụ và vị trí của tiện ích phản chiếu.
Trong quá trình xử lý 4TB dữ liệu mà chúng tôi đã thu thập, một trong những thách thức lớn nhất là việc căn chỉnh tất cả các nguồn hình ảnh/dữ liệu khác nhau. Mỗi video từ máy ảnh DSLR cần được căn chỉnh với Kinect tương ứng để các pixel được căn chỉnh trong không gian cũng như thời gian. Sau đó, cảnh quay từ hai giàn máy quay này cần được căn chỉnh với nhau để tạo thành một nghệ sĩ. Sau đó, chúng tôi cần điều chỉnh nghệ sĩ 3D với dữ liệu được thu thập từ bản vẽ của họ. Chà! Chúng tôi đã viết các công cụ dựa trên trình duyệt để giúp thực hiện hầu hết các tác vụ này và bạn có thể tự thử các công cụ đó tại đây
Sau khi căn chỉnh dữ liệu, chúng tôi đã sử dụng một số tập lệnh được viết bằng NodeJS để xử lý tất cả dữ liệu đó và xuất ra một tệp video và một loạt tệp JSON, tất cả đều được cắt bớt và đồng bộ hoá. Để giảm kích thước tệp, chúng tôi đã làm 3 việc. Trước tiên, chúng tôi giảm độ chính xác của từng số dấu phẩy động để các số này có độ chính xác tối đa là 3 chữ số thập phân. Thứ hai, chúng ta giảm số điểm xuống một phần ba còn 30 khung hình/giây và nội suy các vị trí phía máy khách. Cuối cùng, chúng tôi đã chuyển đổi tuần tự dữ liệu để thay vì sử dụng JSON thuần tuý với các cặp khoá/giá trị, một thứ tự giá trị sẽ được tạo cho vị trí và độ xoay của HMD và tay điều khiển. Thao tác này đã giảm kích thước tệp xuống còn chưa đến 3 MB, đủ để phân phối qua mạng.
Vì bản thân video được phân phát dưới dạng một phần tử video HTML5 được đọc bằng một hoạ tiết WebGL để trở thành các hạt, nên bản thân video cần phải phát ẩn trong nền. Chương trình đổ bóng chuyển đổi màu sắc trong hình ảnh độ sâu thành các vị trí trong không gian 3D. James George đã chia sẻ một ví dụ tuyệt vời về cách bạn có thể làm với cảnh quay ngay trong DepthKit.
iOS có các quy định hạn chế về tính năng phát video cùng dòng. Chúng tôi cho rằng điều này là để ngăn người dùng bị làm phiền bởi quảng cáo dạng video trên web tự động phát. Chúng tôi đã sử dụng một kỹ thuật tương tự như các giải pháp khác trên web, đó là sao chép khung hình video vào canvas và cập nhật thời gian tua video theo cách thủ công, cứ 1/30 giây một lần.
videoElement.addEventListener( 'timeupdate', function(){
videoCanvas.paintFrame( videoElement );
});
function loopCanvas(){
if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){
const time = Date.now();
const elapsed = ( time - lastTime ) / 1000;
if( videoState.playing && elapsed >= ( 1 / 30 ) ){
videoElement.currentTime = videoElement.currentTime + elapsed;
lastTime = time;
}
}
}
frameLoop.add( loopCanvas );
Phương pháp của chúng tôi có một tác dụng phụ đáng tiếc là làm giảm đáng kể tốc độ khung hình iOS vì việc sao chép vùng đệm pixel từ video sang canvas rất tốn CPU. Để giải quyết vấn đề này, chúng tôi chỉ cần phân phát các phiên bản có kích thước nhỏ hơn của cùng một video cho phép tốc độ khung hình tối thiểu là 30 khung hình/giây trên iPhone 6.
Kết luận
Quan điểm chung về việc phát triển phần mềm VR kể từ năm 2016 là giữ cho hình học và chương trình đổ bóng đơn giản để bạn có thể chạy ở tốc độ 90 khung hình/giây trở lên trong HMD. Đây hóa ra là một mục tiêu thực sự tuyệt vời cho các bản minh hoạ WebGL vì các kỹ thuật được sử dụng trong bản đồ Tilt Brush rất phù hợp với WebGL.
Mặc dù trình duyệt web hiển thị lưới 3D phức tạp không thú vị, nhưng đây là bằng chứng về khái niệm có thể kết hợp giữa công việc VR và web.