Xây dựng công cụ 3D HTML5 của riêng bạn

Tác Giả: John Stephens
Ngày Sáng TạO: 22 Tháng MộT 2021
CậP NhậT Ngày Tháng: 19 Có Thể 2024
Anonim
Xây dựng công cụ 3D HTML5 của riêng bạn - Sáng TạO
Xây dựng công cụ 3D HTML5 của riêng bạn - Sáng TạO

Có một số giải pháp sẵn có cho 3D trên web. Tuy nhiên, những giải pháp này không phù hợp với tất cả các trang web và đôi khi bạn cần có toàn quyền kiểm soát từ việc biết thông tin chi tiết về cơ sở mã của riêng bạn. Khi đến lúc xây dựng www.bjork.com, một trang web có mô hình 3D được kết xuất theo thời gian thực tương tác, chúng tôi quyết định xây dựng công cụ của riêng mình để đảm bảo nó hoạt động theo cách chúng tôi muốn.

Đây là một trải nghiệm học tập tuyệt vời và nó cho phép chúng tôi hợp lý hóa công cụ để trang web hoạt động tốt nhất. Hướng dẫn này chia nhỏ các bộ phận thành phần của động cơ với hy vọng rằng nó sẽ làm sáng tỏ 3D. Ngay cả những nhà phát triển có kinh nghiệm nhất cũng có thể thấy việc xây dựng công cụ 3D của riêng họ từ đầu là một nhiệm vụ rất khó khăn vì có một lượng đáng kể toán học và lý thuyết liên quan. Tuy nhiên, một khi bạn hiểu các nguyên tắc cơ bản và lý thuyết đằng sau nó, nó gần như không khó như bạn nghĩ. Hy vọng rằng khi chúng tôi hoàn thành, bạn sẽ sẵn sàng bắt đầu xây dựng trải nghiệm 3D của riêng mình.


Nền tảng của bất kỳ công cụ 3D nào là một phương trình:

scale = focusLength / (z + focusLength)

Đây là phương trình để chuyển từ 3D sang 2D. Nó chiếu từ 3D sang 2D.

Cách bạn sử dụng nó như sau:

var point3D = {x: 100, y: 133, z: 230}; var focusLength = 1000; var scale = focusLength / (point3D.z + focusLength); var point2D = {x: point3D.x * scale, y: point3D.y * scale};

Biết được điều này, chúng tôi có thể bắt đầu xây dựng công cụ 3D dựa trên canvas nhỏ / hiệu quả của mình. Trước khi bạn bắt đầu bất kỳ dự án nào, hãy tìm ra những gì bạn muốn đạt được và bắt đầu vạch ra nó trên giấy. Chúng ta nên tìm ra cách cấu trúc engine 3D này và những gì chúng ta muốn vẽ.

Kết xuất các đối tượng 3D là một trong những tác vụ đáng lo ngại nhất mà CPU có thể đảm nhận, vì vậy chúng tôi phải tối ưu hóa công cụ của mình càng nhiều càng tốt. Cách dễ nhất để tối ưu hóa mã là tuân theo quy tắc KISS (Keep It Simple, Stupid). Nếu bạn giữ cho mã của mình càng đơn giản càng tốt thì rất có thể nó sẽ được tối ưu hóa ngay từ đầu hoặc sau đó sẽ rất dễ tối ưu hóa sau này.

Tôi thực sự ủng hộ lập trình Hướng đối tượng và bộ não của tôi có xu hướng suy nghĩ theo những dòng đó. Vì vậy, chúng ta phải bắt đầu suy nghĩ về những gì chúng ta muốn hoàn thành và những gì chúng ta cần. Đối với công cụ 3D của chúng tôi, chúng tôi sẽ kết xuất các đường. Vì vậy, nếu bạn nghĩ về nó, một đường thẳng được tạo thành từ hai điểm nối với nhau bởi đường thẳng thực tế. Vì vậy, chúng ta cần một lớp Point3D và một lớp Line3D. Tất nhiên sẽ rất tuyệt nếu có thứ gì đó kết hợp điều này lại với nhau, vì vậy chúng ta sẽ tạo một lớp có tên là Scene3D.

Vì vậy, hãy bắt đầu với Scene3D.


function Scene3D (canvas) {this.focalLength = 1000; this.context = canvas.getContext ("2d"); this.sceneWidth = canvas.width; this.sceneHeight = canvas.height; this.points3D = []; this.points2D = []; this.numPoints = 0; this.render = function () {}}

Bây giờ điều này có vẻ kỳ lạ vì chúng ta đã nói về Point3D ở trên nhưng tôi vẫn muốn giữ tất cả dữ liệu vị trí thực tế trong Scene3D. Nói chung là vì sau đó chúng ta có thể thực hiện các thao tác trên dữ liệu điểm này mà không cần nhấn Point3D. Bạn sẽ hiểu ý tôi sau. Chúng tôi sẽ quay lại chức năng kết xuất sau.

Bây giờ chúng ta hãy tạo một lớp Point3D. Vì vậy, bởi vì chúng tôi muốn giữ tất cả dữ liệu điểm 3D trong Scene3D, chúng tôi cần một cách để Point3D có thể truy cập và sửa đổi dữ liệu 3D này. Đối với điều này, chúng tôi sẽ sử dụng getters và setters. Getters và setters là các hàm đặc biệt mà bên ngoài lớp trông giống như bất kỳ thuộc tính cũ nào của lớp. Nhưng khi thuộc tính đó được thiết lập hoặc yêu cầu thì các hàm đó thực sự được gọi. Vì vậy, trong trường hợp của chúng tôi khi vị trí x của Point3D được đặt, chúng tôi muốn đặt vị trí x và cũng cập nhật mảng point3d của Scene3D. Trong JavaScript, bạn đã xác định getters và setters làm như sau:


this .__ defineGetter __ ("x", this.getX); this .__ defineSetter __ ("x", this.setX);

Trong đó this.getX và this.setX là các hàm nên được gọi khi thuộc tính này là "get" hoặc "set".

Vì vậy, hãy viết lớp Point3D:

function Point3D (xVal, yVal, zVal) {var _x = xVal! = undefined? xVal: 0; var _y = yVal! = undefined? yVal: 0; var _z = zVal! = undefined? zVal: 0; var myScene = null; var xIdx; var yIdx; var zIdx; var xIdx2D; var yIdx2D; this.setupWithScene = function (scene) {myScene = scene; var idx = scene.setupPoint (_x, _y, _z); var i3 = idx * 3; var i2 = idx * 2; xIdx = i3; yIdx = i3 + 1; zIdx = i3 + 2; xIdx2D = i2; yIdx2D = i2 + 1; } this.getSceneIdx = function () {return mySceneIdx; } this.getX = function () {return _x; } this.setX = function (value) {if (myScene! = null) myScene.points3D [xIdx] = value; _x = giá trị; } this.getY = function () {return _y; } this.setY = function (value) {if (myScene! = null) myScene.points3D [yIdx] = value; _y = giá trị; } this.getZ = function () {return _z; } this.setZ = function (value) {if (myScene! = null) myScene.points3D [zIdx] = value; _z = giá trị; } this.getX2D = function () {return myScene.points2D [xIdx2D]; } this.getY2D = function () {return myScene.points2D [yIdx2D]; } this .__ defineGetter __ ("sceneIdx", this.getSceneIdx); this .__ defineGetter __ ("x", this.getX); this .__ defineGetter __ ("y", this.getY); this .__ defineGetter __ ("z", this.getZ); this .__ defineSetter __ ("x", this.setX); this .__ defineSetter __ ("y", this.setY); this .__ defineSetter __ ("z", this.setZ); this .__ defineGetter __ ("x2D", this.getX2D); this .__ defineGetter __ ("y2D", this.getY2D);}

Vì vậy, để Point3D biết về Scene3D, nó phải được thêm vào một cảnh. Đổi lại, để Point3D có thể cập nhật mảng point3D của Scene3D, nó phải biết vị trí nơi nó được phép cập nhật dữ liệu. Mảng Scene3D’s point3D là mảng một chiều sẽ có dạng như sau: [x, y, z, x, y, z, ...] Vì vậy, dữ liệu nằm trong bộ 3’s. Có vẻ điên rồ khi làm theo cách này, tại sao không sử dụng một mảng 2D hoặc một mảng Đối tượng? Tuy nhiên điều này quay trở lại quy tắc KISS. Bạn không thể làm sai bằng cách giữ cho cấu trúc dữ liệu của mình càng đơn giản càng tốt (có thể dễ dàng tối ưu hóa hoặc không cần tối ưu hóa).

Hãy xem thiết lập chức năngWithScene:

this.setupWithScene = function (scene) {myScene = scene; var idx = scene.setupPoint (_x, _y, _z); var i3 = idx * 3; var i2 = idx * 2; xIdx = i3; yIdx = i3 + 1; zIdx = i3 + 2; xIdx2D = i2; yIdx2D = i2 + 1;}

Đầu tiên, chúng tôi chỉ đặt myScene. Vì vậy, bây giờ Point3D biết nó thuộc về Scene3D nào. Tiếp theo, chúng tôi đang gọi một hàm mà chúng tôi chưa viết vào Scene3D nhưng những gì nó làm là thêm dữ liệu x, y, z hiện được đặt trên Point3D vào khung cảnh và trả về idx cho điểm đó. Từ chỉ số này, chúng ta có thể tính toán các chỉ số cho dữ liệu 3D và dữ liệu 2D. Vì chúng tôi đang lưu dữ liệu 3D trong cảnh tại sao không lưu dữ liệu 2D?

Hãy thêm chức năng setupPoint vào Scene3D:

this.setupPoint = function (x, y, z) {var returnVal = this.numPoints; this.points2D [this.points2D.length] = 0; this.points2D [this.points2D.length] = 0; this.points3D [this.points3D.length] = x; this.points3D [this.points3D.length] = y; this.points3D [this.points3D.length] = z; this.numPoints ++; trả về returnVal;}

Tại thời điểm này, chúng tôi thực sự có thể bắt đầu viết hàm render cho Scene3D. Tuy nhiên, chúng ta phải quyết định xem cảnh đó sẽ thực sự vẽ lên canvas hay lớp Line3D trong tương lai của chúng ta sẽ vẽ. Bây giờ rõ ràng là vì tôi đã gợi ý cho lớp Line3D, chúng tôi sẽ sử dụng nó. Kiến trúc công cụ 3D của bạn để các đối tượng tự hiển thị giúp nó có thể mở rộng hơn vì bất kỳ lúc nào chúng tôi cũng có thể dễ dàng thêm một mục mới có thể được hiển thị. Ví dụ, nếu chúng ta muốn thêm các Hạt thì chúng ta có thể.

Vì vậy, hãy viết lớp Line3D. Một điều thú vị về Canvas là một đường dẫn của bạn có thể bao gồm nhiều dòng. Vì vậy, chúng tôi sẽ tận dụng điều này. Trên thực tế, vẽ một đường dài sẽ tốt hơn nhiều so với vẽ đường dẫn đó dưới dạng các đoạn đường thẳng riêng lẻ.

function Line3D () {this.colour = "# AAAAAA"; this.points = []; this.startPoint = new Point3D (); this.endPoint = new Point3D (); this.addToScene = function (scene) {for (var i = 0; ithis.points.length; i ++) {this.points [i] .setupWithScene (scene); }} this.addPoint = function (point) {this.points [this.points.length] = point; } this.render = function (context) {context.beginPath (); context.strokeStyle = this.colour; for (var i = 0; ithis.points.length; i ++) {context.lineTo (this.points [i] .x2D, this.points [i] .y2D); } context.stroke (); }}

Để tận dụng lợi thế của việc tạo các đường dẫn dài chứ không chỉ các đoạn đường ngắn, chúng ta phải lưu trữ nhiều điểm trên mỗi đường.

Trước khi có thể bắt đầu hiển thị những dòng này, chúng ta cần thêm chúng vào Scene3D, vì vậy hãy viết một hàm addItem vào Scene3D.

this.addItem = function (item) {this.items [this.items.length] = item; item.addToScene (this);}

Chúng ta cũng phải thêm biến mảng items vào Scene3D:

this.items = [];

Tôi nghĩ bây giờ cuối cùng chúng ta cũng đã sẵn sàng để hoàn thành việc viết hàm render cho Scene3D. Chúng ta phải làm gì trong chức năng kết xuất bên cạnh bản vẽ rõ ràng cho canvas? Chúng ta phải tính toán việc chuyển đổi dữ liệu 3D sang 2D. Vì vậy, cuối cùng chúng ta hãy viết hàm kết xuất.

this.render = function () {var halfWidth = this.sceneWidth * 0.5; var halfHeight = this.sceneHeight * 0.5; for (var i = 0; ithis.numPoints; i ++) {var i3 = i * 3; var i2 = i * 2; var x = this.points3D [i3]; var y = this.points3D [i3 + 1]; var z = this.points3D [i3 + 2]; var scale = this.focalLength / (z + this.focalLength); this.points2D [i2] = x * scale + halfWidth; this.points2D [i2 + 1] = y * scale + halfHeight; } this.context.save (); this.context.fillStyle = "rgb (0, 0, 0);"; this.context.fillRect (0, 0, this.sceneWidth, this.sceneHeight); for (var i = 0; ithis.items.length; i ++) {this.items [i] .render (this.context); } this.context.restore ();}

Việc chuyển đổi từ 3D sang 2D có thể trông quen thuộc vì nó khá chính xác với những gì chúng tôi bắt đầu bài viết này.

Vì vậy, bây giờ chúng tôi đã sẵn sàng để sử dụng công cụ 3D đơn giản của mình. Để sử dụng điều này, chúng tôi sẽ phải làm như sau:

  1. Nhận Canvas mà chúng tôi sẽ vẽ trên
  2. Tạo đối tượng Scene3D
  3. Tạo một số đối tượng Line3D và thêm chúng vào Scene3D
  4. Bắt đầu kết xuất Scene3DD trong một vòng lặp khoảng thời gian

Vì vậy, đây là những gì mã đó sẽ như thế nào. Nó sẽ chạy khi tài liệu HTML được tải:

function onInit () {canvas = document.getElementById ("mainCanvas"); scene = new Scene3D (canvas); var numLinesSegment = pointData.length / 4; var line = new Line3D (); line.colour = "rgb (255, 0, 0)"; for (var i = 0; inumLinesSearies; i ++) {var i4 = i * 4; line.addPoint (new Point3D (pointData [i4], pointData [i4 + 1], pointData [i4 + 2])); } scene.addItem (dòng); setInterval (onRender, 33);} Hàm vòng lặp kết xuất của chúng ta trông giống như sau: function onRender () {scene.render ();}

Một điều bạn có thể thắc mắc là function pointData đến từ đâu? Nói chung, tập dữ liệu 3D rất lớn và vì vậy, tôi thường giữ tập dữ liệu trong tệp .js bên ngoài. Tôi đã lưu trữ tập dữ liệu mà chúng tôi đang sử dụng tại jsdo.it.

Bạn có thể xem ví dụ này mà chúng tôi đã viết tại jsdo.it/MikkoH/RadioHead3DContiguous.

Tuy nhiên, một điều bạn có thể nhận thấy khi chạy mã mà chúng tôi đang viết là nó trông không hề 3D chút nào. Trên thực tế, bạn sẽ chỉ thấy một số đường trông giống như không có gì và nó khá nhỏ. Để mô hình 3D của chúng tôi xoay, mở rộng và di chuyển, chúng tôi phải bắt đầu chuyển đổi mô hình của mình bằng Ma trận 3D. Nghe có vẻ phức tạp nhưng nó thực sự không quá tệ. Một ma trận sẽ chỉ tính toán một vị trí được biến đổi mới cho mỗi điểm.

Điều thú vị về JSDo.it là bạn có thể vào và "fork" (sử dụng và sửa đổi) ai đó sử dụng mã và sửa đổi nó. Vì vậy, đây là những gì tôi đã làm để tạo Matrix3D mà chúng tôi có thể sử dụng cho dự án này. Tôi đã tham gia và phân nhánh một lớp học Ma trận do Masayuki Daijima viết.

Vì vậy, hãy thêm một ma trận vào Scene3D của chúng tôi để chúng tôi có thể bắt đầu xoay cảnh của mình. Cũng sẽ rất tuyệt nếu có các thuộc tính cho RotationX, RotationY và scale trong cảnh của chúng ta, vì vậy chúng ta cũng hãy thêm các thuộc tính đó.

function Scene3D (canvas) {this.matrix = new Matrix3D (); this.rotationX = 0; this.rotationY = 0; this.scale = 1; this.focalLength = 1000; this.context = canvas.getContext ("2d"); this.sceneWidth = canvas.width; this.sceneHeight = canvas.height; this.points3D = []; this.points2D = []; this.numPoints = 0; this.items = []; ...

Bây giờ chúng ta sẽ sửa đổi chức năng kết xuất của Scene3D để sử dụng Matrix3D này để biến đổi các điểm trước khi kết xuất. Bằng cách này, chúng tôi có thể mở rộng mô hình của mình sau đó xoay nó.

this.render = function () {var halfWidth = this.sceneWidth * 0.5; var halfHeight = this.sceneHeight * 0.5; this.matrix.identity (); this.matrix.scale (this.scale, this.scale, this.scale); this.matrix.rotateX (this.rotationX); this.matrix.rotateY (this.rotationY); this.matrix.translate (0, 0, 1000); var biến đổi = this.matrix.transformArray (this.points3D); for (var i = 0; ithis.numPoints; i ++) {var i3 = i * 3; var i2 = i * 2; var x = biến đổi [i3]; var y = biến đổi [i3 + 1]; var z = biến đổi [i3 + 2]; var scale = this.focalLength / (z + this.focalLength); this.points2D [i2] = x * scale + halfWidth; this.points2D [i2 + 1] = y * scale + halfHeight; } this.context.save (); this.context.fillStyle = "rgb (0, 0, 0);"; this.context.fillRect (0, 0, this.sceneWidth, this.sceneHeight); for (var i = 0; ithis.items.length; i ++) {this.items [i] .render (this.context); } this.context.restore ();}

Vì vậy, hãy cùng xem qua nội dung ma trận:

Khi chúng ta nói this.matrix.identity (); chúng tôi chỉ đang "thiết lập lại" Ma trận. Ma trận nhận dạng là một thứ gì đó không có tác dụng gì khi được nhân với một số khác. Nó tương tự như số 1. Vì vậy, ví dụ khi chúng ta làm điều gì đó, dòng 6 * 1 = 6, dòng 1 thực sự không làm gì cả.

Một cái gì đó chúng ta phải nói về một cách nhanh chóng là phép nhân Ma trận không phải là giao hoán. Điều này có nghĩa là A * B! = B * A. Vì vậy, điều này có nghĩa là đối với chúng tôi trong ví dụ của chúng tôi là chúng tôi phải thêm các phép biến đổi của mình theo một thứ tự cụ thể để nó trông đúng. Trước tiên, chúng tôi muốn chia tỷ lệ, xoay, sau đó dịch mô hình của chúng tôi. Vì vậy, đó là những gì chúng tôi làm ở đây:

this.matrix.scale (this.scale, this.scale, this.scale); this.matrix.rotateX (this.rotationX); this.matrix.rotateY (this.rotationY); this.matrix.translate (0, 0 , 1000);

Cuối cùng, chúng ta có thể biến đổi điểm của mình:

var biến đổi = this.matrix.transformArray (this.points3D);

Có một bí mật nhỏ bẩn thỉu mà tôi phải chia sẻ về những gì chúng tôi đang xây dựng.

Hãy nhớ phương trình nền tảng của chúng tôi:

var scale = focusLength / (point3D.z + focusLength);

Điều gì sẽ xảy ra nếu point3D.z trở nên rất nhỏ? Vì vậy, về cơ bản đối tượng của chúng ta đang đi sau chúng ta? Hãy tính toán nó ra. Giả sử độ dài tiêu cự là 1000 và vị trí z của chúng ta là -2000.

quy mô = 1000 / (- 2000 + 1000)
quy mô = 1000 / -1000 = -1

Thang điểm của chúng tôi là -1 !!! Điều đó có nghĩa là mô hình của chúng ta sẽ thực sự bị lật ngược khi nó đi ra phía sau chúng ta. Nó thực sự rất khó tìm kiếm. Những gì bạn có thể làm là kiểm tra xem một đoạn đường có phía sau chúng ta hay không và nếu có, đừng hiển thị nó hoặc những gì bạn có thể làm là gian lận như những gì chúng ta đang làm ở đây và đảm bảo đối tượng luôn ở trước mặt chúng ta . Đó là lý do tại sao chúng tôi dịch mô hình của mình theo 1000 vị trí trước khi hiển thị nó.

Nhưng còn một bí mật nhỏ khác ở đây. Để giữ tốc độ khung hình siêu cao, chúng tôi sẽ không thực hiện Z-Rotation. Z-Rotation là khi bạn xác định trước những đoạn đường nào sẽ được hiển thị trước. Tuy nhiên, bạn có thể giải quyết vấn đề này miễn là toàn bộ mô hình của bạn có cùng một màu, vì bạn sẽ không bao giờ có thể phân biệt được đâu là thứ được hiển thị trước và đâu là được hiển thị sau cùng.

Vì vậy, như bạn có thể thấy chắc chắn có những nơi mà bạn có thể sử dụng engine 3D đơn giản này nhưng điều quan trọng là nó phải hoạt động tốt. Vì vậy, khi bạn thử nghiệm, hãy nhớ quy tắc KISS và bạn sẽ tốt.

Và nếu bạn muốn xem Hollywood phát triển như thế nào trong năm 2012, hãy xem những bộ phim 3D hay nhất.

ChọN QuảN Trị
Báo cáo: Chỉ đường web @media, Ngày 2
ĐọC

Báo cáo: Chỉ đường web @media, Ngày 2

Các lide thuyết trình cho buổi nói chuyện đầy cảm hứng tại Web chỉ đường @media tuần trước đã bắt đầu được đưa lên mạng, cho phép những người bỏ lỡ có thể nắm bắt đư...
9 biểu tượng chữ lồng tốt nhất từng được tạo ra
ĐọC

9 biểu tượng chữ lồng tốt nhất từng được tạo ra

Các biểu trưng chữ lồng tốt nhất rất hiệu quả, thậm chí bạn có thể không nhận thấy một cách có ý thức rằng chúng hoàn toàn là chữ lồng. Chữ lồng ...
Các ứng dụng chỉnh sửa video tốt nhất năm 2021
ĐọC

Các ứng dụng chỉnh sửa video tốt nhất năm 2021

Ngày nay, các ứng dụng chỉnh ửa video tốt nhất không chỉ nhắm đến các chuyên gia làm việc trong lĩnh vực truyền hình và điện ảnh. Chúng cũng là một lự...