diff --git a/404.html b/404.html index aae34fc..0ed23bd 100644 --- a/404.html +++ b/404.html @@ -9,13 +9,13 @@ - +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- + \ No newline at end of file diff --git a/assets/images/robustTreatment-139ebe3cfbc4a5bf153e530ff735e8cf.png b/assets/images/robustTreatment-139ebe3cfbc4a5bf153e530ff735e8cf.png new file mode 100644 index 0000000..02769f9 Binary files /dev/null and b/assets/images/robustTreatment-139ebe3cfbc4a5bf153e530ff735e8cf.png differ diff --git a/assets/js/6865f350.afb191e9.js b/assets/js/6865f350.afb191e9.js deleted file mode 100644 index 38d344b..0000000 --- a/assets/js/6865f350.afb191e9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkryao_blog=self.webpackChunkryao_blog||[]).push([[3025],{3905:(e,t,r)=>{r.d(t,{Zo:()=>u,kt:()=>m});var n=r(7294);function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function c(e){for(var t=1;t=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var s=n.createContext({}),l=function(e){var t=n.useContext(s),r=t;return e&&(r="function"==typeof e?e(t):c(c({},t),e)),r},u=function(e){var t=l(e.components);return n.createElement(s.Provider,{value:t},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},f=n.forwardRef((function(e,t){var r=e.components,o=e.mdxType,i=e.originalType,s=e.parentName,u=a(e,["components","mdxType","originalType","parentName"]),p=l(r),f=o,m=p["".concat(s,".").concat(f)]||p[f]||d[f]||i;return r?n.createElement(m,c(c({ref:t},u),{},{components:r})):n.createElement(m,c({ref:t},u))}));function m(e,t){var r=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var i=r.length,c=new Array(i);c[0]=f;var a={};for(var s in t)hasOwnProperty.call(t,s)&&(a[s]=t[s]);a.originalType=e,a[p]="string"==typeof e?e:o,c[1]=a;for(var l=2;l{r.r(t),r.d(t,{assets:()=>s,contentTitle:()=>c,default:()=>d,frontMatter:()=>i,metadata:()=>a,toc:()=>l});var n=r(7462),o=(r(7294),r(3905));const i={sidebar_position:1},c="\u78b0\u649e\u5904\u7406\u7b80\u4ecb",a={unversionedId:"collision-series/introduction",id:"collision-series/introduction",title:"\u78b0\u649e\u5904\u7406\u7b80\u4ecb",description:"\u78b0\u649e\u68c0\u6d4b\u4e4b\u540e\uff0c\u6211\u4eec\u5e0c\u671b\u8ba9\u7269\u4f53\u56de\u5230\u5408\u6cd5\u7684\u4f4d\u7f6e\uff0c\u4e3b\u8981\u6709\u4e24\u79cd\u5904\u7406\u7684\u601d\u8def\uff1a",source:"@site/docs/collision-series/introduction.md",sourceDirName:"collision-series",slug:"/collision-series/introduction",permalink:"/docs/collision-series/introduction",draft:!1,editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/collision-series/introduction.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",previous:{title:"\u78b0\u649e\u7cfb\u5217",permalink:"/docs/category/\u78b0\u649e\u7cfb\u5217"},next:{title:"VF\u78b0\u649e\u80fd\u91cf",permalink:"/docs/collision-series/vertex_face_collision_energy"}},s={},l=[{value:"\u5185\u70b9\u6cd5\uff08Interior Point Methods\uff09",id:"\u5185\u70b9\u6cd5interior-point-methods",level:2}],u={toc:l},p="wrapper";function d(e){let{components:t,...r}=e;return(0,o.kt)(p,(0,n.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"\u78b0\u649e\u5904\u7406\u7b80\u4ecb"},"\u78b0\u649e\u5904\u7406\u7b80\u4ecb"),(0,o.kt)("p",null,"\u78b0\u649e\u68c0\u6d4b\u4e4b\u540e\uff0c\u6211\u4eec\u5e0c\u671b\u8ba9\u7269\u4f53\u56de\u5230\u5408\u6cd5\u7684\u4f4d\u7f6e\uff0c\u4e3b\u8981\u6709\u4e24\u79cd\u5904\u7406\u7684\u601d\u8def\uff1a"),(0,o.kt)("h2",{id:"\u5185\u70b9\u6cd5interior-point-methods"},"\u5185\u70b9\u6cd5\uff08Interior Point Methods\uff09"))}d.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/6865f350.c00ac9a7.js b/assets/js/6865f350.c00ac9a7.js new file mode 100644 index 0000000..50cb3ae --- /dev/null +++ b/assets/js/6865f350.c00ac9a7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkryao_blog=self.webpackChunkryao_blog||[]).push([[3025],{3905:(e,t,r)=>{r.d(t,{Zo:()=>u,kt:()=>m});var n=r(7294);function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function a(e){for(var t=1;t=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}var c=n.createContext({}),s=function(e){var t=n.useContext(c),r=t;return e&&(r="function"==typeof e?e(t):a(a({},t),e)),r},u=function(e){var t=s(e.components);return n.createElement(c.Provider,{value:t},e.children)},p="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},f=n.forwardRef((function(e,t){var r=e.components,o=e.mdxType,i=e.originalType,c=e.parentName,u=l(e,["components","mdxType","originalType","parentName"]),p=s(r),f=o,m=p["".concat(c,".").concat(f)]||p[f]||d[f]||i;return r?n.createElement(m,a(a({ref:t},u),{},{components:r})):n.createElement(m,a({ref:t},u))}));function m(e,t){var r=arguments,o=t&&t.mdxType;if("string"==typeof e||o){var i=r.length,a=new Array(i);a[0]=f;var l={};for(var c in t)hasOwnProperty.call(t,c)&&(l[c]=t[c]);l.originalType=e,l[p]="string"==typeof e?e:o,a[1]=l;for(var s=2;s{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>d,frontMatter:()=>i,metadata:()=>l,toc:()=>s});var n=r(7462),o=(r(7294),r(3905));const i={sidebar_position:1},a="\u78b0\u649e\u5904\u7406\u7b80\u4ecb",l={unversionedId:"collision-series/introduction",id:"collision-series/introduction",title:"\u78b0\u649e\u5904\u7406\u7b80\u4ecb",description:"\u78b0\u649e\u68c0\u6d4b\u4e4b\u540e\uff0c\u6211\u4eec\u5e0c\u671b\u8ba9\u7269\u4f53\u56de\u5230\u5408\u6cd5\u7684\u4f4d\u7f6e\uff0c\u4e3b\u8981\u6709\u4e24\u79cd\u5904\u7406\u7684\u601d\u8def\uff1a",source:"@site/docs/collision-series/introduction.md",sourceDirName:"collision-series",slug:"/collision-series/introduction",permalink:"/docs/collision-series/introduction",draft:!1,editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/collision-series/introduction.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{sidebar_position:1},sidebar:"tutorialSidebar",previous:{title:"\u78b0\u649e\u7cfb\u5217",permalink:"/docs/category/\u78b0\u649e\u7cfb\u5217"},next:{title:"VF\u78b0\u649e\u80fd\u91cf",permalink:"/docs/collision-series/vertex_face_collision_energy"}},c={},s=[{value:"\u5185\u70b9\u6cd5\uff08Interior Point Methods\uff09",id:"\u5185\u70b9\u6cd5interior-point-methods",level:2},{value:"\u8bba\u6587\u4e2d\u7684\u65b9\u6cd5",id:"\u8bba\u6587\u4e2d\u7684\u65b9\u6cd5",level:2}],u={toc:s},p="wrapper";function d(e){let{components:t,...i}=e;return(0,o.kt)(p,(0,n.Z)({},u,i,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("h1",{id:"\u78b0\u649e\u5904\u7406\u7b80\u4ecb"},"\u78b0\u649e\u5904\u7406\u7b80\u4ecb"),(0,o.kt)("p",null,"\u78b0\u649e\u68c0\u6d4b\u4e4b\u540e\uff0c\u6211\u4eec\u5e0c\u671b\u8ba9\u7269\u4f53\u56de\u5230\u5408\u6cd5\u7684\u4f4d\u7f6e\uff0c\u4e3b\u8981\u6709\u4e24\u79cd\u5904\u7406\u7684\u601d\u8def\uff1a"),(0,o.kt)("h2",{id:"\u5185\u70b9\u6cd5interior-point-methods"},"\u5185\u70b9\u6cd5\uff08Interior Point Methods\uff09"),(0,o.kt)("h2",{id:"\u8bba\u6587\u4e2d\u7684\u65b9\u6cd5"},"\u8bba\u6587\u4e2d\u7684\u65b9\u6cd5"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},"\u9996\u5148\u5bbd\u68c0\u6d4b\uff0c\u5bf9\u8ddd\u79bb\u8fd1\u7684\u53bb\u505a\u4e00\u4e2a\u6392\u65a5\u529b\u5904\u7406\uff0c\u51cf\u5c11\u78b0\u649e\u7684\u53d1\u751f\uff0c\u6392\u65a5\u529b\u8981\u9650\u5236\u5927\u5c0f\uff0c\u4e0d\u80fd\u76f4\u63a5\u5728\u5355\u5e27\u5185\u63a8\u51fa\u91cd\u53e0\u533a\u57df"),(0,o.kt)("li",{parentName:"ul"},"\u6392\u65a5\u529b\u5206\u4e3a\u975e\u5f39\u6027\uff08\u9632\u6b62\u78b0\u649e\uff09\u548c\u5f39\u6027\uff08\u7528\u4e8e\u6a21\u62df\u5e03\u6599\u538b\u7f29\uff09"),(0,o.kt)("li",{parentName:"ul"},"\u6392\u65a5\u529b\u7528\u4e8e\u751f\u6210\u6469\u64e6"),(0,o.kt)("li",{parentName:"ul"},"\u4ee5\u4e0a\u4ea7\u751f\u51b2\u91cf\u7528\u4e8e\u4fee\u6b63\u901f\u5ea6\uff0c\u63a5\u4e0b\u6765\u5f00\u59cb\u68c0\u6d4b\u65f6\u95f4\u6b65\u957f\u4e2d\u7684\u78b0\u649e"),(0,o.kt)("li",{parentName:"ul"},"\u5982\u679c\u8fd8\u6709\u78b0\u649e\u4ea7\u751f\uff0c\u90a3\u5c31\u6b63\u5e38\u5904\u7406"),(0,o.kt)("li",{parentName:"ul"},"\u5bf9\u78b0\u649e\u81ea\u78b0\u649e\u7528Rigid Impact Zone\uff0c\u5c06\u78b0\u649e\u7684\u7247\u5143\u653e\u5165\u4e00\u4e2a\u5217\u8868\uff08Impact Zone\uff09\u4e2d\uff0c\u5217\u8868\u4e2d\u7684\u4e1c\u897f\u5f53\u505a\u521a\u4f53\u8fdb\u884c\u6a21\u62df\u3002\u8fd9\u6837\u505a\u4f1a\u8ba9\u5e03\u6599\u51bb\u8d77\u6765\u4e00\u6837\u4e0d\u771f\u5b9e\uff0c\u4f46\u662f\u57fa\u4e8e\u4e4b\u524d\u7684\u6392\u65a5\u529b\u548c\u78b0\u649e\uff0c\u7ecf\u9a8c\u8868\u660eRigid Impact Zone\u4f1a\u53d8\u5f97\u5f88\u5c0f\u3001\u5b64\u7acb\u4e14\u4f4e\u9891\uff0c\u6240\u4ee5\u5176\u5b9e\u6548\u679c\u8fd8\u597d")),(0,o.kt)("center",null,(0,o.kt)("p",null,(0,o.kt)("img",{alt:"robust",src:r(7801).Z,width:"501",height:"303"}))))}d.isMDXComponent=!0},7801:(e,t,r)=>{r.d(t,{Z:()=>n});const n=r.p+"assets/images/robustTreatment-139ebe3cfbc4a5bf153e530ff735e8cf.png"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.5c2dbdb5.js b/assets/js/runtime~main.5d957af8.js similarity index 99% rename from assets/js/runtime~main.5c2dbdb5.js rename to assets/js/runtime~main.5d957af8.js index 4fb161c..e801c48 100644 --- a/assets/js/runtime~main.5c2dbdb5.js +++ b/assets/js/runtime~main.5d957af8.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,c,f,d,t={},r={};function b(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={id:e,loaded:!1,exports:{}};return t[e].call(c.exports,c,c.exports,b),c.loaded=!0,c.exports}b.m=t,b.c=r,e=[],b.O=(a,c,f,d)=>{if(!c){var t=1/0;for(i=0;i=d)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(r=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);b.r(d);var t={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>t[a]=()=>e[a]));return t.default=()=>e,b.d(d,t),d},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({53:"935f2afb",139:"bde6b81b",496:"8c0e717c",511:"853d7296",519:"ec51423d",533:"b2b675dd",604:"22155049",832:"87ae2219",893:"7839d40f",1138:"c5a29527",1266:"125ec243",1477:"b2f554cd",1713:"a7023ddc",1834:"aacd0ad1",2024:"03f56988",2366:"f967e78e",2476:"2c8e7084",2535:"814f3328",2693:"76550e64",3025:"6865f350",3074:"21c60786",3085:"1f391b9e",3089:"a6aa9e1f",3475:"be874743",3538:"5bf72342",3608:"9e4087bc",3703:"ad587386",3805:"a265bca8",3910:"3396d5c1",3988:"c6d6d603",4013:"01a85c17",4195:"c4f5d8e4",4322:"f5e3a597",4716:"8ba634d3",4776:"ef7c4cf3",4900:"ea0f3eb2",5090:"e7738e50",5150:"9c876981",5210:"ac5f2f59",5432:"ec723fe8",5788:"4c30d0fe",6103:"ccc49370",6171:"6fca9af4",6208:"3f4ca0c2",6219:"575e4a60",6474:"33ab39d4",6947:"7ad2b423",7008:"bb21d70f",7206:"5c3b3aa9",7311:"6d47f0aa",7343:"892abff3",7412:"7ea8078a",7414:"393be207",7561:"bd84e224",7918:"17896441",8259:"1a816252",8401:"22db39bf",8610:"6875c492",8950:"bc522f7e",9514:"1be78505",9581:"314205e2",9671:"0e384e19",9817:"14eb3368"}[e]||e)+"."+{53:"f2348392",139:"ea1302cc",496:"ec606d54",511:"15b16dca",519:"394ad217",533:"1d592338",604:"23cdd8d7",832:"6ea223d1",893:"2a63fb9c",1138:"3651cb71",1266:"f8ed7755",1477:"26951624",1713:"a657fce9",1834:"337124c6",2024:"6f6ee2e4",2366:"999c26d8",2476:"4a54dffd",2529:"1a976a99",2535:"b653edbf",2693:"13ae707d",3025:"afb191e9",3074:"f5ab1051",3085:"60c3856e",3089:"54ae5a34",3475:"943cb370",3538:"cc30440c",3608:"45ecb1b6",3703:"7c52d699",3805:"80c166eb",3910:"b671f4bf",3946:"fe29b4d4",3988:"18eb2e9a",4013:"b24b06c3",4195:"7d326ab3",4322:"53d2d560",4716:"3f6d0941",4776:"7b2c0f96",4900:"079317d1",4972:"ffa93c54",5090:"17426698",5150:"8e7e6577",5210:"903f3267",5432:"4d933abf",5788:"a5662690",6103:"9afd9042",6171:"9c354001",6208:"13233aa9",6219:"8b3efbc2",6474:"3374a8a0",6947:"0ebba826",7008:"b2b55af3",7206:"5a928f9b",7311:"cc155c8d",7343:"a88d1807",7412:"4a0719df",7414:"a52c3f0f",7561:"90d69d8e",7918:"5722986e",8259:"cbe87a39",8401:"3c3a7c30",8610:"c824e621",8950:"834efd11",9514:"aa7a9923",9581:"6c0d6443",9671:"eee46400",9817:"e4b6cfb0"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="ryao-blog:",b.l=(e,a,c,t)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=u.bind(null,r.onerror),r.onload=u.bind(null,r.onload),o&&document.head.appendChild(r)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"7918",22155049:"604","935f2afb":"53",bde6b81b:"139","8c0e717c":"496","853d7296":"511",ec51423d:"519",b2b675dd:"533","87ae2219":"832","7839d40f":"893",c5a29527:"1138","125ec243":"1266",b2f554cd:"1477",a7023ddc:"1713",aacd0ad1:"1834","03f56988":"2024",f967e78e:"2366","2c8e7084":"2476","814f3328":"2535","76550e64":"2693","6865f350":"3025","21c60786":"3074","1f391b9e":"3085",a6aa9e1f:"3089",be874743:"3475","5bf72342":"3538","9e4087bc":"3608",ad587386:"3703",a265bca8:"3805","3396d5c1":"3910",c6d6d603:"3988","01a85c17":"4013",c4f5d8e4:"4195",f5e3a597:"4322","8ba634d3":"4716",ef7c4cf3:"4776",ea0f3eb2:"4900",e7738e50:"5090","9c876981":"5150",ac5f2f59:"5210",ec723fe8:"5432","4c30d0fe":"5788",ccc49370:"6103","6fca9af4":"6171","3f4ca0c2":"6208","575e4a60":"6219","33ab39d4":"6474","7ad2b423":"6947",bb21d70f:"7008","5c3b3aa9":"7206","6d47f0aa":"7311","892abff3":"7343","7ea8078a":"7412","393be207":"7414",bd84e224:"7561","1a816252":"8259","22db39bf":"8401","6875c492":"8610",bc522f7e:"8950","1be78505":"9514","314205e2":"9581","0e384e19":"9671","14eb3368":"9817"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var f=b.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var t=b.p+b.u(a),r=new Error;b.l(t,(c=>{if(b.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),t=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+d+": "+t+")",r.name="ChunkLoadError",r.type=d,r.request=t,f[1](r)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,t=c[0],r=c[1],o=c[2],n=0;if(t.some((a=>0!==e[a]))){for(f in r)b.o(r,f)&&(b.m[f]=r[f]);if(o)var i=o(b)}for(a&&a(c);n{"use strict";var e,a,c,f,d,t={},r={};function b(e){var a=r[e];if(void 0!==a)return a.exports;var c=r[e]={id:e,loaded:!1,exports:{}};return t[e].call(c.exports,c,c.exports,b),c.loaded=!0,c.exports}b.m=t,b.c=r,e=[],b.O=(a,c,f,d)=>{if(!c){var t=1/0;for(i=0;i=d)&&Object.keys(b.O).every((e=>b.O[e](c[o])))?c.splice(o--,1):(r=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[c,f,d]},b.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return b.d(a,{a:a}),a},c=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,b.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var d=Object.create(null);b.r(d);var t={};a=a||[null,c({}),c([]),c(c)];for(var r=2&f&&e;"object"==typeof r&&!~a.indexOf(r);r=c(r))Object.getOwnPropertyNames(r).forEach((a=>t[a]=()=>e[a]));return t.default=()=>e,b.d(d,t),d},b.d=(e,a)=>{for(var c in a)b.o(a,c)&&!b.o(e,c)&&Object.defineProperty(e,c,{enumerable:!0,get:a[c]})},b.f={},b.e=e=>Promise.all(Object.keys(b.f).reduce(((a,c)=>(b.f[c](e,a),a)),[])),b.u=e=>"assets/js/"+({53:"935f2afb",139:"bde6b81b",496:"8c0e717c",511:"853d7296",519:"ec51423d",533:"b2b675dd",604:"22155049",832:"87ae2219",893:"7839d40f",1138:"c5a29527",1266:"125ec243",1477:"b2f554cd",1713:"a7023ddc",1834:"aacd0ad1",2024:"03f56988",2366:"f967e78e",2476:"2c8e7084",2535:"814f3328",2693:"76550e64",3025:"6865f350",3074:"21c60786",3085:"1f391b9e",3089:"a6aa9e1f",3475:"be874743",3538:"5bf72342",3608:"9e4087bc",3703:"ad587386",3805:"a265bca8",3910:"3396d5c1",3988:"c6d6d603",4013:"01a85c17",4195:"c4f5d8e4",4322:"f5e3a597",4716:"8ba634d3",4776:"ef7c4cf3",4900:"ea0f3eb2",5090:"e7738e50",5150:"9c876981",5210:"ac5f2f59",5432:"ec723fe8",5788:"4c30d0fe",6103:"ccc49370",6171:"6fca9af4",6208:"3f4ca0c2",6219:"575e4a60",6474:"33ab39d4",6947:"7ad2b423",7008:"bb21d70f",7206:"5c3b3aa9",7311:"6d47f0aa",7343:"892abff3",7412:"7ea8078a",7414:"393be207",7561:"bd84e224",7918:"17896441",8259:"1a816252",8401:"22db39bf",8610:"6875c492",8950:"bc522f7e",9514:"1be78505",9581:"314205e2",9671:"0e384e19",9817:"14eb3368"}[e]||e)+"."+{53:"f2348392",139:"ea1302cc",496:"ec606d54",511:"15b16dca",519:"394ad217",533:"1d592338",604:"23cdd8d7",832:"6ea223d1",893:"2a63fb9c",1138:"3651cb71",1266:"f8ed7755",1477:"26951624",1713:"a657fce9",1834:"337124c6",2024:"6f6ee2e4",2366:"999c26d8",2476:"4a54dffd",2529:"1a976a99",2535:"b653edbf",2693:"13ae707d",3025:"c00ac9a7",3074:"f5ab1051",3085:"60c3856e",3089:"54ae5a34",3475:"943cb370",3538:"cc30440c",3608:"45ecb1b6",3703:"7c52d699",3805:"80c166eb",3910:"b671f4bf",3946:"fe29b4d4",3988:"18eb2e9a",4013:"b24b06c3",4195:"7d326ab3",4322:"53d2d560",4716:"3f6d0941",4776:"7b2c0f96",4900:"079317d1",4972:"ffa93c54",5090:"17426698",5150:"8e7e6577",5210:"903f3267",5432:"4d933abf",5788:"a5662690",6103:"9afd9042",6171:"9c354001",6208:"13233aa9",6219:"8b3efbc2",6474:"3374a8a0",6947:"0ebba826",7008:"b2b55af3",7206:"5a928f9b",7311:"cc155c8d",7343:"a88d1807",7412:"4a0719df",7414:"a52c3f0f",7561:"90d69d8e",7918:"5722986e",8259:"cbe87a39",8401:"3c3a7c30",8610:"c824e621",8950:"834efd11",9514:"aa7a9923",9581:"6c0d6443",9671:"eee46400",9817:"e4b6cfb0"}[e]+".js",b.miniCssF=e=>{},b.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),b.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},d="ryao-blog:",b.l=(e,a,c,t)=>{if(f[e])f[e].push(a);else{var r,o;if(void 0!==c)for(var n=document.getElementsByTagName("script"),i=0;i{r.onerror=r.onload=null,clearTimeout(s);var d=f[e];if(delete f[e],r.parentNode&&r.parentNode.removeChild(r),d&&d.forEach((e=>e(c))),a)return a(c)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:r}),12e4);r.onerror=u.bind(null,r.onerror),r.onload=u.bind(null,r.onload),o&&document.head.appendChild(r)}},b.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},b.p="/",b.gca=function(e){return e={17896441:"7918",22155049:"604","935f2afb":"53",bde6b81b:"139","8c0e717c":"496","853d7296":"511",ec51423d:"519",b2b675dd:"533","87ae2219":"832","7839d40f":"893",c5a29527:"1138","125ec243":"1266",b2f554cd:"1477",a7023ddc:"1713",aacd0ad1:"1834","03f56988":"2024",f967e78e:"2366","2c8e7084":"2476","814f3328":"2535","76550e64":"2693","6865f350":"3025","21c60786":"3074","1f391b9e":"3085",a6aa9e1f:"3089",be874743:"3475","5bf72342":"3538","9e4087bc":"3608",ad587386:"3703",a265bca8:"3805","3396d5c1":"3910",c6d6d603:"3988","01a85c17":"4013",c4f5d8e4:"4195",f5e3a597:"4322","8ba634d3":"4716",ef7c4cf3:"4776",ea0f3eb2:"4900",e7738e50:"5090","9c876981":"5150",ac5f2f59:"5210",ec723fe8:"5432","4c30d0fe":"5788",ccc49370:"6103","6fca9af4":"6171","3f4ca0c2":"6208","575e4a60":"6219","33ab39d4":"6474","7ad2b423":"6947",bb21d70f:"7008","5c3b3aa9":"7206","6d47f0aa":"7311","892abff3":"7343","7ea8078a":"7412","393be207":"7414",bd84e224:"7561","1a816252":"8259","22db39bf":"8401","6875c492":"8610",bc522f7e:"8950","1be78505":"9514","314205e2":"9581","0e384e19":"9671","14eb3368":"9817"}[e]||e,b.p+b.u(e)},(()=>{var e={1303:0,532:0};b.f.j=(a,c)=>{var f=b.o(e,a)?e[a]:void 0;if(0!==f)if(f)c.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((c,d)=>f=e[a]=[c,d]));c.push(f[2]=d);var t=b.p+b.u(a),r=new Error;b.l(t,(c=>{if(b.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var d=c&&("load"===c.type?"missing":c.type),t=c&&c.target&&c.target.src;r.message="Loading chunk "+a+" failed.\n("+d+": "+t+")",r.name="ChunkLoadError",r.type=d,r.request=t,f[1](r)}}),"chunk-"+a,a)}},b.O.j=a=>0===e[a];var a=(a,c)=>{var f,d,t=c[0],r=c[1],o=c[2],n=0;if(t.some((a=>0!==e[a]))){for(f in r)b.o(r,f)&&(b.m[f]=r[f]);if(o)var i=o(b)}for(a&&a(c);n - +
- + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index 87a0296..cd2a013 100644 --- a/blog/archive.html +++ b/blog/archive.html @@ -9,13 +9,13 @@ - +

Archive

Archive

- + \ No newline at end of file diff --git a/blog/tags.html b/blog/tags.html index e839798..2417404 100644 --- a/blog/tags.html +++ b/blog/tags.html @@ -9,13 +9,13 @@ - +

Tags

- + \ No newline at end of file diff --git "a/blog/tags/\347\241\225\345\243\253\347\224\237\346\264\273.html" "b/blog/tags/\347\241\225\345\243\253\347\224\237\346\264\273.html" index 793e8ca..e87f698 100644 --- "a/blog/tags/\347\241\225\345\243\253\347\224\237\346\264\273.html" +++ "b/blog/tags/\347\241\225\345\243\253\347\224\237\346\264\273.html" @@ -9,13 +9,13 @@ - +

One post tagged with "硕士生活"

View All Tags
- + \ No newline at end of file diff --git a/blog/wait-for-a-stroy.html b/blog/wait-for-a-stroy.html index acf122c..1efe2de 100644 --- a/blog/wait-for-a-stroy.html +++ b/blog/wait-for-a-stroy.html @@ -9,13 +9,13 @@ - +
- + \ No newline at end of file diff --git a/docs/algorithm-series/heap_sort.html b/docs/algorithm-series/heap_sort.html index 745bec6..0f21e91 100644 --- a/docs/algorithm-series/heap_sort.html +++ b/docs/algorithm-series/heap_sort.html @@ -9,14 +9,14 @@ - +

堆排序

参考: 【排序算法:堆排序【图解+代码】】 https://www.bilibili.com/video/BV1fp4y1D7cj/?share_source=copy_web&vd_source=ee33f825ba0d9765eddc91a10101fa78

#include <iostream>
#include <vector>

using namespace std;

// 交换数组中两个元素的位置
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}


// 维护堆的性质
// n: 数组长度
// i: 要维护的节点
void heapify(vector<int>& arr, int n, int i)
{
int largest = i;
int lson = 2 * i + 1;
int rson = 2 * i + 2;

if (lson < n && arr[lson] > arr[largest])
largest = lson;
if (rson < n && arr[rson] > arr[largest])
largest = rson;

if (largest != i)
{
swap(arr[largest], arr[i]);
heapify(arr, n, largest);
}
}

// 堆排序接口
void heapSort(vector<int>& arr)
{
// 建堆
int n = arr.size();
for (int i = n / 2 - 1; i >= 0; i--)
{
// 从第一个最后一个非叶子节点开始,维护堆的性质
heapify(arr, n, i);
}

// 堆排序
while (n > 1)
{
swap(arr[0], arr[--n]);
heapify(arr, n, 0);
}

}

// 测试
int main() {
vector<int> arr = { 7, 2, 1, 6, 8, 5, 3, 4 };
heapSort(arr);
cout << "排序结果:";
for (int num : arr) {
cout << num << " ";
}
cout << endl;

return 0;
}
- + \ No newline at end of file diff --git a/docs/algorithm-series/merge_sort.html b/docs/algorithm-series/merge_sort.html index c757d06..de99b61 100644 --- a/docs/algorithm-series/merge_sort.html +++ b/docs/algorithm-series/merge_sort.html @@ -9,13 +9,13 @@ - +

归并排序

参考:【排序算法:归并排序【图解+代码】】 https://www.bilibili.com/video/BV1Pt4y197VZ/?share_source=copy_web&vd_source=ee33f825ba0d9765eddc91a10101fa78

// 合并
void merge(vector<int>& arr, vector<int>& tempArr, int low, int mid, int high)
{
// 标记左右半区第一个未排序的元素
// 临时数组的下标
int lPtr = low;
int rPtr = mid + 1;
int p = low;

// 合并
while (lPtr <= mid && rPtr <= high)
{
if (arr[lPtr] < arr[rPtr])
tempArr[p++] = arr[lPtr++];
else
tempArr[p++] = arr[rPtr++];
}

// 合并左半区剩余的元素
while (lPtr <= mid)
tempArr[p++] = arr[lPtr++];

// 合并右半区剩余的元素
while (rPtr <= high)
tempArr[p++] = arr[rPtr++];

// 把临时数组中合并后的元素复制粘贴到原数组中
for (int i = low; i <= high; i++)
{
arr[i] = tempArr[i];
}
}

void mergeSort(vector<int>& arr, vector<int>& tempArr, int low, int high)
{
// 只有一个元素就不划分
if (low < high)
{
int mid = (low + high) / 2;
// 递归划分
mergeSort(arr, tempArr, low, mid);
mergeSort(arr, tempArr, mid + 1, high);
// 合并左右半区
merge(arr, tempArr, low, mid, high);
}
}

void mergeSort(vector<int>& arr)
{
int size = arr.size();
vector<int> tempArr(size);
mergeSort(arr, tempArr, 0, size - 1);
}

// 测试
int main() {
vector<int> arr = { 5,1,1,2,0,0 };
mergeSort(arr);
return 0;
}
- + \ No newline at end of file diff --git a/docs/algorithm-series/quick_sort.html b/docs/algorithm-series/quick_sort.html index 25cef08..230a3e7 100644 --- a/docs/algorithm-series/quick_sort.html +++ b/docs/algorithm-series/quick_sort.html @@ -9,13 +9,13 @@ - +

快排

#include <iostream>
#include <vector>

using namespace std;

// 交换数组中两个元素的位置
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

// 分割数组并返回分割点的索引
int partition(vector<int>& arr, int low, int high) {
int pivot = arr[high]; // 数组最后一位为基准值
int smallRightBound = low - 1; // 小于基准值的区域的有边界,初始时认为没有任何值小于基准值,所以边界是-1

for (int i = low; i < high; ++i)
{
if (arr[i] < pivot)
{
++smallRightBound;
swap(arr[smallRightBound], arr[i]); // 将小于基准值的值swap到右边界
}
}
swap(arr[smallRightBound + 1], arr[high]); // 将基准值放到右边界的右侧
return smallRightBound + 1;
}

// 快速排序的递归函数
void quickSort(vector<int>& arr, int low, int high) {
if (low < high) // 退出条件
{
int pivot = partition(arr, low, high);
quickSort(arr, pivot + 1, high);
quickSort(arr, low, pivot - 1);
}
}

// 快速排序的接口函数
void quickSort(vector<int>& arr) {
int size = arr.size();
quickSort(arr, 0, size - 1);
}

// 测试
int main() {
vector<int> arr = { 7, 2, 1, 6, 8, 5, 3, 4 };
quickSort(arr);

cout << "排序结果:";
for (int num : arr) {
cout << num << " ";
}
cout << endl;

return 0;
}
- + \ No newline at end of file diff --git a/docs/algorithm-series/union_find.html b/docs/algorithm-series/union_find.html index 6f5ae2a..0cc0fd8 100644 --- a/docs/algorithm-series/union_find.html +++ b/docs/algorithm-series/union_find.html @@ -9,14 +9,14 @@ - +

并查集

适合用于检查两个元素是否属于一个集合,以及两个元素之间是否有连通路径(连通路径还可以用广度优先搜索和深度优先搜索来查)。 一个例子:

unionFind

class UnionFind {
public:
UnionFind(int n) {
parent = vector<int>(n);
rank = vector<int>(n);
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}

void uni(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx]++;
}
}
}

int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}

bool connect(int x, int y) {
return find(x) == find(y);
}
private:
vector<int> parent;
vector<int> rank;
};

class Solution {
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
if (source == destination) {
return true;
}
UnionFind uf(n);
for (auto edge : edges) {
uf.uni(edge[0], edge[1]);
}
return uf.connect(source, destination);
}
};
- + \ No newline at end of file diff --git "a/docs/category/cuda\347\263\273\345\210\227.html" "b/docs/category/cuda\347\263\273\345\210\227.html" index 925454d..cc72e86 100644 --- "a/docs/category/cuda\347\263\273\345\210\227.html" +++ "b/docs/category/cuda\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/c\347\263\273\345\210\227.html" "b/docs/category/c\347\263\273\345\210\227.html" index 3bb4f72..6013d65 100644 --- "a/docs/category/c\347\263\273\345\210\227.html" +++ "b/docs/category/c\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/gpu\347\263\273\345\210\227.html" "b/docs/category/gpu\347\263\273\345\210\227.html" index 01f5f36..0a22786 100644 --- "a/docs/category/gpu\347\263\273\345\210\227.html" +++ "b/docs/category/gpu\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - +
- + \ No newline at end of file diff --git "a/docs/category/jointsolver\347\263\273\345\210\227.html" "b/docs/category/jointsolver\347\263\273\345\210\227.html" index 22d9d87..96db5eb 100644 --- "a/docs/category/jointsolver\347\263\273\345\210\227.html" +++ "b/docs/category/jointsolver\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/math\347\263\273\345\210\227.html" "b/docs/category/math\347\263\273\345\210\227.html" index c463ca2..2697f7a 100644 --- "a/docs/category/math\347\263\273\345\210\227.html" +++ "b/docs/category/math\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/pbd\347\263\273\345\210\227.html" "b/docs/category/pbd\347\263\273\345\210\227.html" index d759388..ae46164 100644 --- "a/docs/category/pbd\347\263\273\345\210\227.html" +++ "b/docs/category/pbd\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/ue\347\263\273\345\210\227.html" "b/docs/category/ue\347\263\273\345\210\227.html" index 30e039e..dfd168f 100644 --- "a/docs/category/ue\347\263\273\345\210\227.html" +++ "b/docs/category/ue\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/unity\347\263\273\345\210\227.html" "b/docs/category/unity\347\263\273\345\210\227.html" index 5a3ba50..05233a3 100644 --- "a/docs/category/unity\347\263\273\345\210\227.html" +++ "b/docs/category/unity\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/\344\273\277\347\234\237\344\270\255\347\232\204\346\234\254\346\236\204\346\250\241\345\236\213.html" "b/docs/category/\344\273\277\347\234\237\344\270\255\347\232\204\346\234\254\346\236\204\346\250\241\345\236\213.html" index 9a58cb7..5a53224 100644 --- "a/docs/category/\344\273\277\347\234\237\344\270\255\347\232\204\346\234\254\346\236\204\346\250\241\345\236\213.html" +++ "b/docs/category/\344\273\277\347\234\237\344\270\255\347\232\204\346\234\254\346\236\204\346\250\241\345\236\213.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/\347\242\260\346\222\236\347\263\273\345\210\227.html" "b/docs/category/\347\242\260\346\222\236\347\263\273\345\210\227.html" index f4051d8..0baccd9 100644 --- "a/docs/category/\347\242\260\346\222\236\347\263\273\345\210\227.html" +++ "b/docs/category/\347\242\260\346\222\236\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git "a/docs/category/\347\256\227\346\263\225\347\263\273\345\210\227.html" "b/docs/category/\347\256\227\346\263\225\347\263\273\345\210\227.html" index 95f005c..bcf4d86 100644 --- "a/docs/category/\347\256\227\346\263\225\347\263\273\345\210\227.html" +++ "b/docs/category/\347\256\227\346\263\225\347\263\273\345\210\227.html" @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git a/docs/collision-series/introduction.html b/docs/collision-series/introduction.html index 5ece7c2..82d170b 100644 --- a/docs/collision-series/introduction.html +++ b/docs/collision-series/introduction.html @@ -9,13 +9,13 @@ - +
-

碰撞处理简介

碰撞检测之后,我们希望让物体回到合法的位置,主要有两种处理的思路:

内点法(Interior Point Methods)

- +

碰撞处理简介

碰撞检测之后,我们希望让物体回到合法的位置,主要有两种处理的思路:

内点法(Interior Point Methods)

论文中的方法

  • 首先宽检测,对距离近的去做一个排斥力处理,减少碰撞的发生,排斥力要限制大小,不能直接在单帧内推出重叠区域
  • 排斥力分为非弹性(防止碰撞)和弹性(用于模拟布料压缩)
  • 排斥力用于生成摩擦
  • 以上产生冲量用于修正速度,接下来开始检测时间步长中的碰撞
  • 如果还有碰撞产生,那就正常处理
  • 对碰撞自碰撞用Rigid Impact Zone,将碰撞的片元放入一个列表(Impact Zone)中,列表中的东西当做刚体进行模拟。这样做会让布料冻起来一样不真实,但是基于之前的排斥力和碰撞,经验表明Rigid Impact Zone会变得很小、孤立且低频,所以其实效果还好

robust

+ \ No newline at end of file diff --git a/docs/collision-series/vertex_face_collision_energy.html b/docs/collision-series/vertex_face_collision_energy.html index f85ac4d..20cc4b6 100644 --- a/docs/collision-series/vertex_face_collision_energy.html +++ b/docs/collision-series/vertex_face_collision_energy.html @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git a/docs/constitutive-model-series/arap.html b/docs/constitutive-model-series/arap.html index aeef612..7d2b9fc 100644 --- a/docs/constitutive-model-series/arap.html +++ b/docs/constitutive-model-series/arap.html @@ -9,14 +9,14 @@ - +

ARAP

参考论文:Sorkine, O. and M. Alexa (2007). As-rigid-as-possible surface modeling. In Eurog. Symposium on Geometry processing, Volume 4.

ARAP(As-Rigid-As-Possible)的定义和弹簧质点模型中的能量非常相似,被称作是“Most-Spring-Mass-Like in F-based World”,具体如下所示:

ΨARAP=μ2FRF2\Psi_{\text{ARAP}} =\frac{\mu}{2}||{\bf F}-{\bf R}||_F^2

其中的R\bf R使用极分解得到。

形变梯度的极分解

对于一个形变梯度F\bf F,我们可以对其进行分解,得到一个正交矩阵R\bf R和一个半正定矩阵S\bf S

F=RS{\bf F} = {\bf R}{\bf S}

其中正交矩阵R\bf R是形变梯度中的旋转,S\bf S则是其中的非旋转部分(缩放)。

在具体实现中,可以使用svd分解来获得正交矩阵R\bf R,然后计算ARAP的能量密度:

REAL ARAP::psi(const MATRIX3 &U, const VECTOR3 &Sigma, const MATRIX3 &V) const {
const MATRIX3 F = U * Sigma.asDiagonal() * V.transpose();
// R = U * V.transpose()
return 0.5 * _mu * (F - U * V.transpose()).squaredNorm();
}

PK1 (First Piola-Kirchhoff Stress Tensor)

ARAP的能量密度对形变梯度的一阶偏导非常简单:

PARAP(F)=μ(FR)P_{\text{ARAP}}(F) = \mu ({\bf F}-{\bf R})

Hessian

ARAP的能量密度及其对形变梯度的一阶偏导都比较简单,麻烦的是能量密度对形变梯度的二阶导(hessian),可以先尝试计算一下:

2ΨARAPF2=PARAPF=μF(FR)=μ(IRF)\frac{\partial^2\Psi_{\text{ARAP}}}{\partial {\bf F}^2}=\frac{\partial P_{\text{ARAP}}}{\partial {\bf F}}=\mu\frac{\partial}{\partial {\bf F}}({\bf F}-{\bf R})=\mu(I-\frac{\partial{\bf R}}{\partial{\bf F}})

这里面有一个正交矩阵R\bf R对形变梯度的求导,对极分解这个过程求微分是十分困难的。在Dynamic Deformables中使用了一系列不变式(invariants)来表征形变中的某些属性,然后使用不变式构成了各个本构模型的能量密度,并给出了hessian的计算通式。基于这个计算通式,我们可以成功计算出ARAP的能量密度对形变梯度的二阶偏导。(如果你还不了解这一套基于不变式的计算方法,可以查看我的这一篇文档:《不变式构造本构模型并计算Hessian》

首先使用三个不变式来重写ARAP能量:

ΨARAP=I22I1+3\Psi_{\text{ARAP}}=I_2-2I_1+3

从而可以计算得到能量密度函数相对各个不变式的一阶、二阶导:

ΨARAPI1=22ΨARAPI12=0ΨARAPI2=12ΨARAPI22=0ΨARAPI3=02ΨARAPI32=0\begin{aligned} &\frac{\partial \Psi_{\text{ARAP}}}{\partial I_1} = -2 \qquad & &\frac{\partial^2 \Psi_{\text{ARAP}}}{\partial I_1^2} = 0\\ &\frac{\partial \Psi_{\text{ARAP}}}{\partial I_2} = 1 \qquad & &\frac{\partial^2 \Psi_{\text{ARAP}}}{\partial I_2^2} = 0\\ &\frac{\partial \Psi_{\text{ARAP}}}{\partial I_3} = 0 \qquad & &\frac{\partial^2 \Psi_{\text{ARAP}}}{\partial I_3^2} = 0 \end{aligned}

最后使用计算通式就可以直接获得Hessian:

vec(2ΨF2)=i=132ΨIi2gigiT+ΨIiHi=2I9×92H1\operatorname{vec}\left(\frac{\partial^2 \Psi}{\partial {\bf F}^2}\right)=\sum_{i=1}^3\frac{\partial^2\Psi}{\partial I_i^2}{\bf g}_i{\bf g}_i^T+\frac{\partial \Psi}{\partial I_i}{\bf H}_i=2{\bf I}_{9\times9}-2{\bf H_1}

代码中的实现如下:

    MATRIX9 dPdF;
dPdF.setIdentity();
// in the paper(Dynamic Deformables, Siggraph Course 2022),
// 5.5.2 p73
dPdF *= _mu;

// calculate the hessian
dPdF -= lambda0 * (q0 * q0.transpose());
dPdF -= lambda1 * (q1 * q1.transpose());
dPdF -= lambda2 * (q2 * q2.transpose());
dPdF *= 2;
- + \ No newline at end of file diff --git a/docs/constitutive-model-series/eigen_system.html b/docs/constitutive-model-series/eigen_system.html index dcf61fa..ea57213 100644 --- a/docs/constitutive-model-series/eigen_system.html +++ b/docs/constitutive-model-series/eigen_system.html @@ -9,7 +9,7 @@ - + @@ -82,7 +82,7 @@ s-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7 c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z M834 80h400000v40h-400000z">1U001000100VT

后面三个特征值对应的特征矩阵为:

Qi{6,7,8}=j=02zjDjwhere{z0=σxσz+σyλiz1=σyσz+σxλiz2=λi2=σz2{\bf Q}_{i\in\{6,7,8\}}=\sum_{j=0}^2z_j{\bf D}_j \qquad \text{where} \left \{ \begin{aligned} &z_0=\sigma_x\sigma_z+\sigma_y\lambda_i\\ &z_1=\sigma_y\sigma_z+\sigma_x\lambda_i\\ &z_2=\lambda_i^2=\sigma_z^2 \end{aligned} \right.

其中,矩阵Dj{\bf D}_j为:

D0=U[100000000]VTD1=U[000010000]VTD2=U[000000001]VT{\bf D_0}={\bf U}\begin{bmatrix} 1&0&0\\0&0&0\\0&0&0 \end{bmatrix}{\bf V}^T \qquad {\bf D_1}={\bf U}\begin{bmatrix} 0&0&0\\0&1&0\\0&0&0 \end{bmatrix}{\bf V}^T \qquad {\bf D_2}={\bf U}\begin{bmatrix} 0&0&0\\0&0&0\\0&0&1 \end{bmatrix}{\bf V}^T

不过话说回来,为什么要算特征矩阵呢?不是只要把所有负的特征值映射到上就可以了么?是因为我们在映射之后还需要使用特征值和特征矩阵组合起来得到新的能量密度的Hessian去参与线性系统的解算。重组Hessian的公式为:

vec(RF)=i=02λivec(Qi)vec(Qi)T\operatorname{vec}\left(\frac{\partial {\bf R}}{\partial {\bf F}}\right)= \sum_{i=0}^2\lambda_i\operatorname{vec}({\bf Q}_i)\operatorname{vec}({\bf Q}_i)^T
- + \ No newline at end of file diff --git a/docs/constitutive-model-series/invariants.html b/docs/constitutive-model-series/invariants.html index 87f698d..0a8905c 100644 --- a/docs/constitutive-model-series/invariants.html +++ b/docs/constitutive-model-series/invariants.html @@ -9,7 +9,7 @@ - + @@ -51,7 +51,7 @@ c-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z M834 80h400000v40h-400000z">1U001000100VTλ3...8=0Q3...8=subspace orthogonal to Q0,1,2

那么根据特征值系统的性质就可以得到:

vec(RF)=i=02λivec(Qi)vec(Qi)T\operatorname{vec}\left(\frac{\partial {\bf R}}{\partial {\bf F}}\right)= \sum_{i=0}^2\lambda_i\operatorname{vec}({\bf Q}_i)\operatorname{vec}({\bf Q}_i)^T

结论

基于上述内容,可以给出本构模型的计算通用方法如下:

g1=vec(R)H1=i=02λivec(Qi)vec(Qi)Tg2=vec(2F)H2=2I9×9g3=[f1×f2f2×f0f0×f1]H3=[03×3f^2f^1f^203×3f^0f^1f^003×3]\begin{aligned} &{\bf g}_1=\operatorname{vec}({\bf R}) &{\bf H}_1=\sum_{i=0}^2\lambda_i\operatorname{vec}({\bf Q}_i)\operatorname{vec}({\bf Q}_i)^T\\ &{\bf g}_2=\operatorname{vec}(2{\bf F}) &{\bf H}_2=2{\bf I}_{9\times9}\\ &{\bf g}_3=\left[ \begin{array}{c|c|c} {\bf f}_1\times{\bf f}_2 & {\bf f}_2\times{\bf f}_0 & {\bf f}_0\times{\bf f}_1 \end{array} \right] &{\bf H}_3=\begin{bmatrix} {\bf 0}_{3\times3} & -\hat{\bf f}_2 & \hat{\bf f}_1\\ \hat{\bf f}_2 & {\bf 0}_{3\times3} & -\hat{\bf f}_0\\ -\hat{\bf f}_1 & \hat{\bf f}_0 & {\bf 0}_{3\times3} \end{bmatrix} \end{aligned}
  1. 使用不变式I1,I2I_1, I_2I3I_3来重写能量密度函数Ψ\Psi
  2. 计算能量密度函数相对不变量的标量导数:ΨI1\frac{\partial \Psi}{\partial I_1}2ΨI12\frac{\partial^2\Psi}{\partial I_1^2}ΨI2\frac{\partial \Psi}{\partial I_2}2ΨI22\frac{\partial^2\Psi}{\partial I_2^2}ΨI3\frac{\partial \Psi}{\partial I_3}2ΨI32\frac{\partial^2\Psi}{\partial I_3^2}
  3. 使用下面两个通式来计算能量密度的和Hessian: vec(2ΨF2)=i=132ΨIi2gigiT+ΨIiHi\operatorname{vec}\left(\frac{\partial^2 \Psi}{\partial {\bf F}^2}\right)=\sum_{i=1}^3\frac{\partial^2\Psi}{\partial I_i^2}{\bf g}_i{\bf g}_i^T+\frac{\partial \Psi}{\partial I_i}{\bf H}_i
- + \ No newline at end of file diff --git a/docs/cpp-series/rvalue_reference.html b/docs/cpp-series/rvalue_reference.html index 7c2a4b0..5b0c447 100644 --- a/docs/cpp-series/rvalue_reference.html +++ b/docs/cpp-series/rvalue_reference.html @@ -9,7 +9,7 @@ - + @@ -18,7 +18,7 @@ move这个词看上去像是做了资源的移动,但是没有,move其实就是一个类型转换。如产品preference所说:

In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.

move(x)产生一个将亡值(xvalue)表达式来标识其参数x。他就完全等同于 static_cast<T&&>(x)。所以说,move 并不作任何的资源转移操作。单纯的move(x)不会有任何的性能提升,不会有任何的资源转移。它的作用仅仅是产生一个标识x的右值表达式。因为它会返回一个右值,所以可以和一个右值引用进行绑定:

int a = 2;
int&& rref = std::move(a);

它们有什么用?

到这里,可能会发现右值引用以及 move 好像都也没什么用,凸显不出它跟左值引用有什么特殊点。其实他们主要用在函数参数里面,下面是一个cppreference的例子:

void f(int& x)
{
std::cout << "lvalue reference overload f(" << x << ")\n";
}

void f(const int& x)
{
std::cout << "lvalue reference to const overload f(" << x << ")\n";
}

void f(int&& x)
{
std::cout << "rvalue reference overload f(" << x << ")\n";
}

int main()
{
int i = 1;
const int ci = 2;
f(i); // calls f(int&)
f(ci); // calls f(const int&)
f(3); // calls f(int&&)
// would call f(const int&) if f(int&&) overload wasn't provided
f(std::move(i)); // calls f(int&&)

// rvalue reference variables are lvalues when used in expressions
int&& x = 1;
f(x); // calls f(int& x)
f(std::move(x)); // calls f(int&& x)
}

当函数参数既有左值引用重载,又有右值引用重载的时候,我们得到重载规则如下:

  • 若传入参数是非const左值,调用非const左值引用重载函数
  • 若传入参数是const左值,调用const左值引用重载函数
  • 若传入参数是右值,调用右值引用重载函数(即使是有 const 左值引用重载的情况下) 因此,f(3)f(std::move(i))会调用f(int&&),因为他们提供的入参都是右值。

所以,通过 move 语义 和 右值引用的配合,我们能提供右值引用的重载函数。这给我们一个机会,一个可以利用右值的机会。特别是对于 xvalue(将亡值)来说,他们都是即将销毁的资源,如果我们能最大程度利用这些资源的话,这显然会极大的增加效率、节省空间。

移动构造函数

之前提到,单纯的 move 不会带来任何资源转移,那么要怎么实现转移函数呢? 考虑一个简单的string类,提供了构造函数和拷贝构造函数:

class string {
string(const char* a, length) {
m_length = length;
m_ptr = malloc(m_length);
memcpy(a, m_ptr, length);
}

string(const string& b) {
m_length = b.m_length;
m_ptr = malloc(m_length);
memcpy(m_ptr, b.m_ptr, b.length);
}

char* m_ptr;
int m_length;
};

注意,由于类中使用了指针m_ptr,所以在拷贝构造函数里面要使用深拷贝,即重新申请内存空间,并将其内存数据用memcpy拷贝过来。

如果我们在程序中需要构建一个存储了这个 string 类的数组,可能需要这么做:

vector<string> list;
string a("hello world", 11);
// 这里会调用拷贝构造函数, 将 a 对象拷贝一份,vector 再把这个副本添加到 vector 中
list.push_back(a);

加入到数组后,a这个对象就没有用了,那么我们希望能够把a对象的资源移动,而不是重新拷贝一份,这样的话相比能够提高效率。有两个问题:

  • push_back 函数如何通过入参来区分对象是应该拷贝资源还是应该移动资源
  • 如何用已有的 string 对象通过资源转移构造出另一个 string,而不是调用拷贝构造函数

关于问题一,事实上我们知道右值可以用来标识对象即将要销毁,所以只要能够区分参数是右值还是左值就可以知道用移动还是构造了。根据之前提到的重载规则,我们需要为push_back提供右值引用的重载,从而右值会优先调用到右值引用参数的函数。

void push_back(string&& v) {
// ...
}

那么要如何产生右值来调用重载的函数呢?使用 move 语义就可以,std::move(a)会产生一个将亡值。

接下来思考问题二,我们使用右值引用作为参数来重载构造函数来解决该问题:

string(string&& b) {
m_length = b.m_length;
m_ptr = b.m_ptr;
b.m_ptr = nullptr;
}

这个函数就叫做移动构造函数。它的参数是右值引用,并且从实现中可以看到,并没有像拷贝构造函数那样重新调用 malloc 申请资源,而是直接用了另一个对象的堆上的资源。也就是在移动构造函数中,才真正完成了资源的转移。根据前面左右引用函数重载的规则,要想调用移动构造函数,那么必须传入参数为右值才行。使用 move 可以将左值转换为右值:

string a("hello world", 11);
list.push_back(std::move(a));

事实上,STL中的 vector 容器已经提供了右值引用的push_back重载,不需要我们来自己实现。

什么时候需要实现移动构造函数?

对比之前给出的移动构造函数和拷贝构造函数,可以发现它们大多数地方都是相同的复制操作。其实,只要是栈上的资源,都是采用复制的方式,只有堆上的资源,才能够复用旧的对象的资源

为什么栈上的资源不能复用,而要重新复制一份?因为你不知道旧的对象何时析构,旧的对象一旦析构,其栈上所占用的资源也会完全被销毁掉,新的对象如果复用的这些资源就会产生崩溃。

为什么堆上的资源可以复用?因为堆上的资源不会自动释放,除非你手动去释放资源。可以看到,在移动构造函数特意将旧对象的m_ptr指针置为 null,就是为了预防外面对其进行 delete 释放资源。

所以说,只有当你的类申请到了堆上的内存资源的时候,才需要专门实现移动构造函数,否则其实没有必要,因为他的消耗跟拷贝构造函数是一模一样的。

- + \ No newline at end of file diff --git a/docs/cpp-series/template.html b/docs/cpp-series/template.html index 862e519..e80e905 100644 --- a/docs/cpp-series/template.html +++ b/docs/cpp-series/template.html @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git a/docs/cpp-series/virtual_table.html b/docs/cpp-series/virtual_table.html index e2c7eab..3c927db 100644 --- a/docs/cpp-series/virtual_table.html +++ b/docs/cpp-series/virtual_table.html @@ -9,14 +9,14 @@ - +

多态和虚函数表

最近连续几次面试都问了这个问题,于是将其记录到博客中。

什么是多态

“多态”(polymorphism),是指计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。简单地来说,就是“在用父类指针调用函数时,实际调用的是指针指向的实际类型(子类)的成员函数”。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的。

举例:

class Base {
public:
virtual void vir_func() { cout << "virtual function, this is base class!" << endl; }
void func() { cout << "normal function, this is base class!" << endl; }
}

class A : public Base {
virtual void vir_func() { cout << "virtual function, this is A class!" << endl; }
void func() { cout << "normal function, this is A class!" << endl; }
}

class B : public Base {
virtual void vir_func() { cout << "virtual function, this is B class!" << endl; }
void func() { cout << "normal function, this is B class!" << endl; }
}

int main() {
Base* base = new Base();
Base* a = new A();
Base* b = new B();
base->func();
a->func();
b->func();
cout << "========================================" << endl;
base->vir_func();
a->vir_func();
b->vir_func();
}

代码运行的结果为:

normal function, this is base class!
normal function, this is base class!
normal function, this is base class!
========================================
virtual function, this is base class!
virtual function, this is A class!
virtual function, this is B class!

总结一下上面的规律:当使用基类的指针调用成员函数的时候,普通函数由指针的类型来决定,虚函数由指针指向的实际类型决定。这个功能是通过虚函数表来实现的。

虚函数表

解释虚函数表的原理之前,先介绍一下类的内存分布,对于一个不包含静态变量和虚函数的类:

class noVir{
public:
void func_a();
void func_b();
int var;
}

它的内存分布是这样的:

noVirtualMemory

其中成员函数放在代码区,为该类的所有对象公有,即不管新建多少个该类的对象,所对应的都是同一个函数存储区的函数。而成员变量则为各个对象所私有,即每新建一个对象都会新建一块内存区用来存储var值。在调用成员函数时,程序会根据类的类型,找到对应代码区所对应的函数并进行调用。在文章开头的例子中,base、a、b都是Base类型的指针。调用普通函数时,程序根据指针的类型到类Base所对应的代码区找到所对应的函数,所以都调用了类Base的func函数,即指针的类型决定了普通函数的调用。

而带有虚函数的类的内存分布是这样的:

class withVir{
public:
void func_a();
virtual void func_b();
int var;
}

virtualMemory

如果使用sizeof(withVir)可以发现,withVir类会比noVir类大四个字节,多出来的这部分内容就是指针vptr,该指针叫做虚函数表指针,它指向一个名为虚函数表(vtbl)的表。 虚函数表实际上一个数组,数组里面的每个元素都是一个函数指针。上例中,虚函数表里就存储了虚函数func_b()具体实现所对应的位置。

注意,普通函数、虚函数、虚函数表都是同一个类的所有对象公有的,只有成员变量和虚函数表指针是每个对象私有的,sizeof的值也只包括vptr和var所占内存的大小,并且vptr通常会在对象内存的最起始位置。

不论一个类中有多少个虚函数,类的实例中也只会有一个vptr指针,增多虚函数,变化的是该类所对应的虚函数表的长度,即其中所存储的指向虚函数的函数指针的数量。

那么可以总结出虚函数的实现原理:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。如开头例子中,当调用vir_func函数时,分别通过base、a、b指针找到对应的vptr,然后找到各自的虚函数表vtbl,最后通过vtbl找到各自虚函数的具体实现。所以虚函数的调用时由指针所指向内存块的具体类型决定的。

构造函数和析构函数可以是虚函数吗?

给出结论:构造函数不能是虚函数,析构函数可以是、且推荐写为虚函数

为什么构造函数不能是虚函数?我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区,所以构造函数只能作为普通函数存放在类所指定的代码区中。

为什么析构函数推荐最好设置为虚函数?如文章开头的例子中,当我们delete(a)的时候,如果析构函数不是虚函数,那么调用的将会是基类Base的析构函数。而当继承的时候,通常派生类会在基类的基础上定义自己的成员,基类的析构函数并不知道派生类中有什么新的成员,自然也无法将它们的内存释放,所以说析构函数会被推荐写为虚函数。

- + \ No newline at end of file diff --git a/docs/cuda-series/CUDA_framework.html b/docs/cuda-series/CUDA_framework.html index 8441c89..5ba7491 100644 --- a/docs/cuda-series/CUDA_framework.html +++ b/docs/cuda-series/CUDA_framework.html @@ -9,13 +9,13 @@ - +

单文件CUDA程序的基本框架

单文件情况下,一个典型的CUDA程序的基本框架如下:

// include headers
// define constant or macro
// C++ function and CUDA kernel function declare

int main() {
// 1. Allocate memory (host and device)
// 2. Initialize the data in the host
// 3. Copy some data from host to device
// 4. Call kernel function
// 5. Copy some data from device to host
// 6. Free memory (host and device)
}

// C++ function and CUDA kernel function define
- + \ No newline at end of file diff --git a/docs/cuda-series/CUDA_thread_organization.html b/docs/cuda-series/CUDA_thread_organization.html index cf92ba1..0e9f296 100644 --- a/docs/cuda-series/CUDA_thread_organization.html +++ b/docs/cuda-series/CUDA_thread_organization.html @@ -9,7 +9,7 @@ - + @@ -20,7 +20,7 @@ 所以,在上述程序中,主机只 指派了设备的一个线程,网格大小和线程块大小都是 11,即 1×1=11\times 1=1

grid&amp;block&amp;thread

这里的网格、线程块和线程大致与硬件结构中的GPU、SM(流式多处理器)和SP(流式处理器)一一对应:

hardware

调用核函数后,程序调用了一个CUDA运行时API函数cudaDeviceSynchronize(),该函数能够促使缓冲区刷新,从而将之前存放在缓冲区的输出流的内容输出出来。

多线程

一般来说,总的线程数大于计算核心数的时候才能够更充分地利用GPU中的计算资源,因为这会让计算和内存访问合理地重叠,从而减小计算核心空闲的时间。

通过改变网格大小(线程块数量)和线程块大小(单个块中线程数量)能够改变指派的线程数量。 核函数中代码的执行方式是“单指令-多线程:,即每一个线程都执行同一串指令。所以通过下述代码就可以在屏幕上打印8行同样的文字。

hello_from_gpu<<<2, 4>>>();

Tips: 从开普勒架构开始,最大允许的线程块大小是1024,一维网格的最大允许的网格大小是23112^{31}-1。虽然一个核函数允许指派的线程数目是巨大的,但是执行时能够同时活跃的线程数是由硬件(CUDA核心数)和软件(核函数中的代码)共同决定的。所以为了高效率的运行代码,除了指派足够的线程,还需要一些写核函数的技巧。

线程索引

每个线程在核函数里都有一个唯一的身份标识,该身份标识由两个参数决定:

  • blockIdx.x:这个变量代表一个当前线程在一个网格中所属于的线程块的编号,取值范围是[0, gridDim.x-1]gridDim.x即之前指派的网格大小。
  • threadIdx.x:这个变量代表一个当前线程在所属于的线程块中的编号,取值范围是[0, blockDim.x-1]blockDim.x即之前指派的线程块大小。

推广到多维网格

从之前索引中出现的.x应该就可以猜到,之前的定义的网格都是一维的结构。并且gridDimblockIdxblockDimthreadIdx都是结构体。

blockIdxthreadIdx的类型为uint3,该类型为一个结构体,具有x、y、z三个成员,其定义为:

struct __device_builtin__ uint3 {
unsigned int x, y, z;
};
typedef __device_builtin__ struct uint3 uint3

gridDimblockDim的类型为dim3,该类型也为一个结构体,具有x、y、z三个成员,并且还包括了一些成员函数。

在之前的例子中,我们使用的执行配置只使用了两个整数,这两个这整数的值会分别赋给内建变量gridDim.xblockDim.x,其他未被指定的成员则默认为1。这个情况下,网格和线程块都是一维的。我们可以给dim3的三个成员全部赋值然后实现多维的网格和线程块:

//任何未被指定的成员都会默认为1
dim3 grid_size(Gx, Gy, Gz);
dim3 block_size(Bx, By, Bz);

多维的网格和线程块本质还是一维的(和数组一样),我们可以这样计算出一个线程的一维编号(在线程块中):

int tid = threadIdx.z * blockDim.x * blockDim.y +
threadIdx.y * blockDim.x + threadIdx.x;

这里需要注意,这样的一维编号定义并不能扩展到一维线程块中去,因为各个线程块的执行是相互独立的。

instance

对于不同的代码需求,有时候可能会需要不同的符合线程索引。

Tips: CUDA中对能够定义的网格大小和线程块大小做了限制,对任何从开普勒到安培架构的GPU来说,网格大小在x、y、z这3个方向上的最大允许值分别为23112^{31}-165535655356553565535。线程块在x、y、z这3个方向上的最大允许值分别为1024、1024和64,并且要求线程块的总大小,即blockDim.xblockDim.yblockDim.z的乘积不能大于1024。

线程束(thread warp)

一个线程块还可以细分成多个线程束,一个线程束(也就是一束线程)是一个线程块里面相邻的warpSize个线程。warpSize也是一个内建变量,其值对于目前所有的GPU架构都是32。所以,一个线程束就是连续的32个线程。

一般来说,希望线程块的大小是warpSize的整数倍,否则系统会自动为剩下的n个线程补齐32-n个线程,形成一个完整的线程束,而这32-n个线程并不会被核函数调用,从而闲置。

- + \ No newline at end of file diff --git a/docs/gpu-series/animation_optimize.html b/docs/gpu-series/animation_optimize.html index dddfbca..b075c55 100644 --- a/docs/gpu-series/animation_optimize.html +++ b/docs/gpu-series/animation_optimize.html @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@

优化骨骼蒙皮动画

骨骼蒙皮动画的流程

主要可以分为以下几个阶段:

  • 播放动画阶段:动画控制器会根据关键帧信息等,调整骨骼的空间属性(旋转、缩放、平移)
  • 计算骨骼矩阵阶段:从根骨骼开始,根据层级关系,逐一计算出每一根骨骼的转换矩阵
  • 蒙皮阶段:更新网格上每个顶点的属性。网格的顶点根据权重被骨骼影响,游戏中一般一个顶点被最多四个骨骼影响
  • 渲染阶段:当顶点变换到角色坐标系下后,就可以进行渲染了。这里与一次普通的渲染没什么太大差别,唯一需要注意的是,Unity不会对蒙皮网格渲染器进行合批,所以每一个骨骼蒙皮动画实例都至少需要一次DrawCall

animation

这其中的各个阶段都带来一定的负载,有两种主流的优化方案:烘焙顶点动画烘焙骨骼矩阵动画。他们的基本思路都是将骨骼蒙皮动画的结果余弦保存在一张纹理中,然后在运行时通过 GPU 从这张纹理中采样,并且使用采样接过来更新顶点属性;再结合实例化(GPU Instancing)来达到高效、大批量渲染的目的。

烘焙顶点动画

可以简单的将它的工作流程分为两个阶段:

  • 非运行状态下的烘焙阶段
  • 运行状态下的播放阶段

烘焙阶段

主要思路即:使用一个表来记录每一个顶点在每一个关键帧时的位置表,然后在播放阶段的时候进行读取顶点的位置并进行更新,就等于完成了蒙皮工作。使用这种方式来更新角色动画,其实是直接使用了预先处理好的骨骼动画、蒙皮网格渲染器的作用结果,是一种用空间换时间的策略。

那么为了便于 GPU 读取顶点位置表,我们可以将数据保存为一张纹理,比如,对于一个拥有505个顶点的模型来说,我们可以将表表中的信息保存到一张 512 x Height 大小的纹理中。

这其中,纹理的宽度用来表示顶点的数量,而纹理的高度用来表示关键帧,所以Height的值取决于动画长度以及动画帧率。

由于动画播放时,顶点的实时位置是从纹理中采样,而非从网格中读取的(不再使用蒙皮网格渲染器,顶点缓冲区内的数据不会被修改),所以顶点属性中的法线信息也无法使用了(永远是静止状态下的);如果需要获取正确的法向量,那就需要在烘焙顶点坐标时也同样将法线烘焙下来,并在顶点变换阶段将这个法向量也采样出来。

如果存在多个动画(例如空闲、移动、攻击),如果每一个动画都烘焙两个纹理(顶点位置和法向量),那贴图的数量很快就会不受控制。 鉴于所有动画对应的顶点数量一致,也就是纹理的宽度都相同,我们可以将多个动画纹理进行合并。

bake

动画播放阶段

我们通过UV坐标来获取这张纹理上的像素,就可以被理解为:取第U个顶点在第V帧时的坐标。在播放动画时,CPU将当前播放的关键帧传给顶点着色器;顶点着色器计算出对应的V坐标;结合顶点索引及动画纹理的宽度计算出U,既可采样出这个顶点基于角色坐标系下的坐标;接下来用这个坐标再进行后面的空间变换就可以了。

动画过渡

简单的动画过渡很容易实现,只要在切换动画时,分别计算出当前动画和下一个动画的播放位置,然后传给GPU进行两次顶点位置采样,再对两次采样的结果进行插值即可。

使用实例化渲染

实例化渲染的特点是使用相同网格相同材质,通过不同的实例属性完成大批量的带有一定差异性的渲染;而烘焙顶点恰好符合了实例化渲染的使用需求。

所以,我们只需将控制动画播放的关键属性:比如过渡动画播放的V坐标、当前和下一个动画的插值比例等,放入实例化数据数组中进行传递;再在顶点着色器中,对关键属性获取并使用即可。

与传统方法比较

  • 不再需要CPU计算动画和蒙皮,提升了性能
  • 可以通过实例化技术批量化渲染角色,减少DrawCall

烘焙顶点的主要问题

  • 模型顶点数量受限:如果纹理的最大尺寸限制在2048 x 2048,那么只能烘焙下顶点数在2048个以下的模型
  • 记录顶点动画的纹理过大
  • 存储的动作长度有限

烘焙骨骼矩阵

除了烘焙顶点,另一种常用的优化方案是烘焙骨骼矩阵动画。

烘焙阶段

听名字就知道,烘焙骨骼矩阵与烘焙顶点位置,原理十分相似;最大的差异在于它们在烘焙时所记录的内容不一样:烘焙顶点记录下来的是每个顶点的位置,而烘焙骨骼矩阵记录下来的是每一根骨骼的矩阵,仅此而已。

烘焙骨骼矩阵最大的意义在于它补上了烘焙顶点的短板:受顶点数量限制、烘焙的动画纹理过大 及 纹理数量较多,因为骨骼的数量很少。 在移动平台上,通常20根左右的骨骼就可以取得不错的表现效果,所以相对于烘焙顶点,烘焙骨骼可以记录下更长的动画,同时它也不再受顶点数量的限制,也无需对法线或切线进行特殊处理(因为可以在采样后通过矩阵计算得出)。

bakeBone

s
  1. 烘焙骨骼矩阵记录的是每根骨骼的ComponentSpace矩阵
  2. 需要将每个顶点与骨骼的关系记录到网格信息中,这个关系是指顶点会被哪根骨骼影响(骨骼索引)以及影响的大小(权重值)
  3. 对于不同的骨骼动画,烘焙矩阵的方式也不一定相同,例如,如果骨骼动画中每根骨骼只会相对于上层骨骼进行旋转变换,那我们烘焙一个四元数就够了

播放阶段

播放阶段烘焙骨骼矩阵的方法会比烘焙顶点动画要多一些计算。例如,在烘焙阶段将完整的矩阵保存在三个像素中,那转换的时候就需要采样三次才能拼凑出一个完整的矩阵。(每个像素四个数据,RGB+Alpha)

- + \ No newline at end of file diff --git a/docs/gpu-series/draw_call.html b/docs/gpu-series/draw_call.html index b8bd111..ad6d187 100644 --- a/docs/gpu-series/draw_call.html +++ b/docs/gpu-series/draw_call.html @@ -9,7 +9,7 @@ - + @@ -18,7 +18,7 @@ 在渲染前,可以先进行视锥体剔除,减少了顶点着色器对不可见顶点的处理次数,提高了GPU的效率。

其弊端在于:合批后的网格会常驻内存,在有些场景下可能并不适用。比如森林中的每一棵树的网格都相同,如果对它采用静态合批策略,合批后的网格基本等同于:单颗树网格 x 树的数量,这对内存的消耗可能就十分巨大了。

总而言之,静态合批在解决场景中材质基本相同、网格不同、且自始至终都保持静止的物体上时,很适用。

动态合批

动态合批没有像静态合批打包时的预处理阶段,它指挥在程序运行时发生。主要用于处理一些模型简单、材质相同、处在运动下的物体。

动态合批会在每次绘制前,先将可以合批的对象整理在一起,然后将它们的网格信息进行合并,接着仅向 GPU 发送一次绘制指令,就可以完成它们整体的绘制。

小Tips

  1. 动态合批不会在绘制前创建新的网格,只是将参与合批的顶点属性连续填充到一块顶点和索引缓冲区中,让 GPU 认为它们是一个整体
  2. 合批前,由于这些对象可能属于不同的父节点,所以需要在送进渲染管线前将每个顶点的坐标转换为世界坐标系下的坐标。

动态合批的条件

动态合批要求:

  • 材质球相同
  • Mesh顶点数量不能超过300以及顶点属性不能超过900
  • 缩放不能为负值(x、y、z向量的乘积不能为负)等

和静态合批的差别:

  1. 动态合批不会创建常驻内存的“合并后网格”,也就是说它不会在运行时造成内存的显著增长,也不会影响打包时的包体大小
  2. 动态合批在绘制前会先将顶点转换到世界坐标系下,然后再填充进顶点、索引缓冲区;静态合批后子网格不接受任何变换操作,仅手动合批后的Root节点可被操作,因此静态合批的顶点、索引缓冲区中的信息不会被修改(Root的变换信息则会通过Constant Buffer传入)
  3. 因为2的原因,动态合批的主要开销在于遍历顶点进行空间变换时的对CPU性能的开销;静态合批没有这个操作,所以也没有这个开销
  4. 动态合批使用根据渲染器类型分配的公共缓冲区,而静态合批使用自己专用的缓冲区。

实例化渲染

很多场景中往往存在大量重复性的元素:树木、草和岩石。 它们都使用了相同的模型,或者模型的种类很少,比如:树可能只有几种;但为了做出差异化,它们的颜色略有不同,高低参差不齐,当然位置也各不相同。

使用静态合批来处理它们(假设它们都没有动画),是不合适的。因为数量太多了,所以合并后的网格体积可能非常大,这会引起内存的增加;而且,这个合并后的网格还是由大量重复网格组成的,不划算。

使用动态合批来处理他们,虽然不会“合并”网格,但是仍然需要在渲染前遍历所有顶点,进行空间变换的操作;虽然单颗树、石头的顶点数量可能不多,但由于数量很多,所以也会在一定程度上增加CPU性能的开销,没必要。

对于场景中这些模型重复、数量众多的渲染需求,可以使用实例化渲染的方法来解决这个问题。

工作原理

实例化渲染,是通过调用“特殊”的渲染接口,由GPU完成的“批处理”。

它与传统的渲染方式相比,最大的差别在于:调用渲染命令时需要告知GPU这次渲染的次数(绘制N个)。当GPU接到这个命令时,就会连续绘制N个物体到我们的屏幕上,其效率远高于连续调用N次传统渲染命令的和(一次绘制一个)。

举个例子,假设希望在屏幕上绘制出两个颜色、位置均不同的箱子。如果使用传统的渲染,则需要调用两次渲染命令(DrawCall = 2),分别为:画一个红箱子 和 画一个绿箱子。如果使用实例化渲染,则只需要调用一次渲染命令(DrawCall = 1),并且附带一些参数2(表示绘制两个)、两个箱子各自的位置(矩阵)、颜色即可。

与静态、动态合批的差异

静、动态合批实质上是将可以合批的对象真正的合并成一个大物体后,再通知GPU进行渲染,也就是其顶点索引缓冲区中必须包含全部参与合批对象的顶点信息;因此,可以认为是CPU完成的批处理。 本质上讲:动、静态合批解决的是合批问题,也就是先有大量存在的单位,再通过一些手段合并成为批次。

而实例化渲染其实是个复制的事儿,是对网格信息的重复利用,从少量复制为大量,只是利用了它“可以通过传入属性实现差异化”的特点,在某些条件下达到了与合批相同的效果。

方法的选择

如果你的场景中存在多数静止的、使用了不同网格、相同材质的物体,特别是当你的相机通常只能照到一部分物体时(如第一视角),可以优先尝试下静态合批,通过牺牲一些内存来提升渲染效率;

针对那些运动的、网格顶点数很少、材质相同的物体,比如飞行的各种箭矢、炮弹等,使用动态合批,通过增加一些CPU处理顶点的性能开销,来提升渲染效率,也许是不错的选择;

如果有大量模型相同、材质相同、或尽管表现上有一些不同,但仍然可以通过属性来实现这些差异化的物体时,启用实例化渲染通常可以在很大程度上提升渲染效率。

- + \ No newline at end of file diff --git a/docs/gpu-series/gems-5-1.html b/docs/gpu-series/gems-5-1.html index 3ff2b91..04c6d06 100644 --- a/docs/gpu-series/gems-5-1.html +++ b/docs/gpu-series/gems-5-1.html @@ -9,14 +9,14 @@ - +

[GPU Gems 3笔记] Part V-1: Real-Time Rigid Body Simulation on GPUs

刚体模拟基础

刚体运动主要包括位移和旋转两个部分。其中位移非常简单,就是质心的移动。当一个力FF作用在一个刚体上,这会引起其动量(linear momentum)PP的变化,具体地:

dPdt=F.\frac{dP}{dt} = F.

根据动量可以获得速度:

v=PMv = \frac{P}{M}

关于旋转,这个力FF作用在刚体上同样也会带来角动量(angular momentum)LL的变化,具体地:

dLdt=r×F\frac{dL}{dt} = r \times F

其中rr是力的作用点和质心的相对位置。

根据角动量可以获得角速度ω\omega

ω=I(t)1L\omega = I(t)^{-1}L

其中I(t)1I(t)^{-1}是刚体在时间t的惯性张量,它是一个3×33\times3的矩阵。惯性张量是会随着刚体的姿态变化的,所以我们需要在每一个仿真步长对其进行更新。而具体到每个时间t的惯性张量的力,可以用下式得到:

I(t)1=R(t)I(0)1R(t)TI(t)^{-1} = R(t)I(0)^{-1}R(t)^T

其中R(t)R(t)是时间t时旋转矩阵,一般来说我们会使用四元数来存储旋转,所以这一步需要一些转换。而四元数的计算可以由角速度得到:

dq=[cos(θ/2),asin(θ/2)]dq = [\operatorname{cos}(\theta/2), a\cdot \operatorname{sin}(\theta / 2)]

其中a=ω/ωa=\omega/|\omega|是旋转轴,θ=ωt\theta = \omega t是旋转角。

刚体形状表达

为了加速碰撞运算,本文选择使用一系列粒子来表示刚体。

具体做法:首先使用3D体素来近似的表示这个rigidbody(通过划分3D网格),然后在每一个体素放一个粒子。这个生成过程可以在GPU中进行加速,首先打一组平行光到刚体上,光线到刚体上的第一个交点构成了一个深度图,第二个交点构成了第二个深度图。那么很明显第一个深度图就表示刚体正面,第二个深度图表示刚体的反面。那么我们将体素作为输入,通过检测这些体素的深度,哪些体素的深度在两个深度图之间,哪些体素就在刚体内,那么就可以在这里生成一个粒子。

depthpeeling

碰撞检测

将刚体用粒子进行表示之后,碰撞检测就被简化为了粒子之间的碰撞检测。这样有一个好处就是碰撞检测很简单,另一个好处就是碰撞检测的精度和速度都是可控的,如果要更大的精度,就可以调小粒子半径,如果要更快的速度就可以用更大的粒子半径。

另一方面,可以使用空间哈希来进行优化,通过选择合适的网格大小,能够让计算效率最大化,一般来说网格的边长是粒子的半径的两倍。

碰撞响应

粒子之间的碰撞力使用离散元(DEM)方法计算得到,这是一种用于计算颗粒材料的方法。粒子之间的斥力fi,sf_{i,s}由一个线性弹簧进行模拟,阻尼力fi,df_{i,d}用一个阻尼器来进行模拟。对于一组碰撞粒子i和j,这些力的计算方法如下:

fi,s=k(drij)rijrijfi,d=ηvijf_{i,s} = -k(d-|r_{ij}|)\frac{r_{ij}}{|r_{ij}|}\\ f_{i,d}=\eta v_{ij}

其中的k,η,d,rij,vijk,\eta,d,r_{ij},v_{ij}分别是弹簧弹性系数,阻尼系数,粒子直径,粒子的相对位置和相对速度。 同时还可以模拟剪切力,它与相对切向速度vij,tv_{ij,t}成正比:

fi,t=ktvij,tf_{i,t}=k_tv_{ij,t}

其中这个相对切向速度的计算方法为:

vij,t=vij(vijrijrij)rijrijv_{ij,t}=v_{ij}-\left(v_{ij}\cdot\frac{r_{ij}}{|r_{ij}|}\right)\frac{r_{ij}}{|r_{ij}|}

通过将力累积就可以获得作用与当前刚体的碰撞力和力矩:

Fc=iRigidBody(fi,s+fi,d+fi,t)Tc=iRigidBody(ri×(fi,s+fi,d+fi,t))F_c = \sum_{i\in RigidBody}(f_{i,s}+f_{i,d}+f_{i,t})\\ T_c = \sum_{i\in RigidBody}(r_i\times (f_{i,s}+f_{i,d}+f_{i,t}))

其中rir_i是当前粒子i相对刚体质心的相对位置。

GPU上的刚体模拟

GPUFramework

具体算法的流程图如上,主要包括:

  1. Computation of particle values
  2. Grid generation
  3. Collision detection and reaction
  4. Computation of momenta
  5. Computation of position and quaternion

其中大部分工作都是内存排不相关的处理,暂不做过多了解。

应用场景

  1. 用于模拟颗粒材料,力直接驱动粒子的位移
  2. 流体模拟,加速SPH的粒子邻近搜索
  3. 流固耦合
- + \ No newline at end of file diff --git a/docs/gpu-series/gems-5-4.html b/docs/gpu-series/gems-5-4.html index dd4c132..f7f193d 100644 --- a/docs/gpu-series/gems-5-4.html +++ b/docs/gpu-series/gems-5-4.html @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@

[GPU Gems 3笔记] Part V-4: Broad-Phase Collision Detection with CUDA

常用Broad-Phase碰撞检测算法

Sort and Sweep

将各个物体的Bounding Volume投影到x、y或者z轴上,在各个轴上生成一个一维碰撞区间[bi,ei][b_i,e_i]。如果这个碰撞区间在三个轴上都没有相交,那么就不可能产生碰撞。具体的检测过程可以对碰撞区间进行排序来完成。很适合用于静态场景的检测。

sap

Spatial Subdivision

空间哈希,一般来说网格会比最大的那个物体要大一些。通过选择一个合适的网格大小,对一个物体,就只需要检测他所在的网格以及与其相邻的网格。如果场景中的物体大小差别巨大,那就效率就会降低。 在比较理想的情况下,碰撞检测仅在以下情况才会进行:物体i和物体j出现在同一个网格中,并且至少有一个物体的中心点也在这个网格中时,才会进行碰撞检测。

spatialhash

Parallel Spatial Subdivision

并行化会使算法变得稍微复杂。

第一个复杂之处在于,如果单个对象与多个网格重叠且这些网格被并行处理,那么该对象可能同时参与多个碰撞测试。因此,必须存在某种机制,以防止两个或多个计算线程同时更新同一对象的状态。为了解决这个问题,我们需要控制每个网格的大小(大于最大物体的bounding volume),由于每个网格的大小至少与计算中最大对象的边界体积相同,因此只要在计算过程中处理的每个网格都与同一时间处理的其他网格至少相隔一个网格,就可以保证每个对象只有一个包含它的网格会被更新。 在2D中,这意味着需要进行四个计算过程来覆盖所有可能的网格;在3D中,需要进行八个过程。

parallelsh

- + \ No newline at end of file diff --git a/docs/gpu-series/gems-5.html b/docs/gpu-series/gems-5.html index e9fae32..02bc59d 100644 --- a/docs/gpu-series/gems-5.html +++ b/docs/gpu-series/gems-5.html @@ -9,13 +9,13 @@ - +

[GPU Gems 3笔记] Part V: Physics Simulation

章节原文内容来自于:https://developer.nvidia.com/gpugems/gpugems3/part-v-physics-simulation

物理模拟是一种高度数据并行化和计算密集型的任务,适合用于GPU计。另一方面,物理仿真计算得到的结果也会直接被GPU用于可视化,所以直接在GPU中进行计算,在graphics memory中生成结果也是很有意义的。

GPU Gems3关于物理模拟的章节和对应笔记:

- + \ No newline at end of file diff --git a/docs/intro.html b/docs/intro.html index 352bd43..62b80c2 100644 --- a/docs/intro.html +++ b/docs/intro.html @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git a/docs/joint_solver-series/fake_cloth.html b/docs/joint_solver-series/fake_cloth.html index bf1eb72..6286a2d 100644 --- a/docs/joint_solver-series/fake_cloth.html +++ b/docs/joint_solver-series/fake_cloth.html @@ -9,13 +9,13 @@ - + - + \ No newline at end of file diff --git a/docs/joint_solver-series/joint_base.html b/docs/joint_solver-series/joint_base.html index 88667db..7c95e7a 100644 --- a/docs/joint_solver-series/joint_base.html +++ b/docs/joint_solver-series/joint_base.html @@ -9,7 +9,7 @@ - + @@ -22,7 +22,7 @@ A点是世界坐标原点,B是骨架的根节点,C是手肘关节节点的父节点,D是手肘关节节点。我们考虑D的变换,那么有:

  • D相对A的变换就是世界空间下的变换
  • D相对C的变换就是本地空间下的变换
  • D相对B的变换就是组件空间下的变换

需要注意,全文所有求解过程都是在组件空间下完成的。

成员函数

初始化 Init()

首先需要对求解器去做一个初始化,在基类中需要初始化的内容不多,将成员变量赋初始值/分配容量就行。由于不同的求解器可能有不同的属性要进行初始化,所以应当被声明为虚函数。

virtual void Init() {
_jointPosition.resize(_dataInterface->GetJointNumber());
_jointRotations.resize(_dataInterface->GetJointNumber());
_subStep = _dataInterface->GetSubStep();
}

_alpha在什么地方初始化?

初始化Transform InitTransform()

接下来在求解之前,首先我们要获得当前骨架中各个骨骼的位置和旋转信息,赋值给Joint Solver的成员变量。

void InitTransform() {
for (int jointIndex = 0; jointIndex < jointNumber; jointIndex++) {
_jointPositions[jointIndex] = _dataInterface->GetInputJointPosition(jointIndex);
_jointRotations[jointIndex] = _dataInterface->GetInputJointRotation(jointIndex);
}
}

重置约束 ResetConstrains()

然后根据在InitTransform()中更新的骨架初始位置和旋转对场景中存在的各个约束进行重置。另外,在重置约束的时候,可能时间步长也是需要的信息之一。

void ResetConstrains() {
for (int constrainIndex = 0; constraintIndex < constraintNumber; constraintIndex++) {
_dataInterface->GetConstrain(constrainIndex)->Reset(_jointPositions, _jointRotations, _deltaT);
}
}

隐式求解 SolveImpl()

接下来就可以直接开始求解得到新的位置和旋转了,这一块就需要具体的求解器,即框架的子类,来具体实现了,我们一般可以定义其为纯虚函数。

virtual void SolveImpl() = 0;

为约束和碰撞求解做准备 PrepareSubStep()

和模拟里面的思路类似,首先先计算得到了一个单次更新的位置和旋转之后,就可以进行碰撞和约束求解(碰撞也定义为一种约束)了。 那么在进行subStep的计算之前,首先要做一些准备,之前的求解中,我们只计算并且更新了骨架中的骨骼的位置和旋转,接下来我们要在这里一并把骨骼上附加的碰撞体也去做对应的位置和旋转的更新。

更新的过程主要是遍历场景中所有的碰撞体,然后记录下这个碰撞体在时间步长开始前、以及SolveImpl之后的碰撞体的Transform,然后使用alpha进行插值,获得在当前这个subStep的Transform估计值,以参与后续的碰撞和约束相关的运算。

各个求解器可能会在该函数中做一些其他运算,所以定义为虚函数。

virtual void PrepareSubStep() {
for (int colliderIndex = 0; colliderIndex < colliderNumber; colliderIndex++) {
Transform colliderTransform, preColliderTransform;
Collider* collider = _dataInterface->GetCollider(colliderIndex);
// 在设置骨骼的碰撞体的时候,该碰撞体本身可能就会有一个和骨骼之间的relative transform
// 所以在collider中会设置一个变量attachOffsetTransform,将这个relative transform给记录下来
// 同样,collider中也会有一个变量attachBoneIndex来记住它所依附的骨骼的编号
colliderTransform = collider->attachOffsetTransform *
_dataInterface->GetJointTransform(collider->attachBoneIndex);
preColliderTransform = collider->attachOffsetTransform *
_dataInterface->GetInputJointTransform(collider->attachBoneIndex);
// 插值,找到对应的时刻的Transform, _alpha在Solve中进行计算
colliderTransform = Lerp(colliderTransform, preColliderTransform, 1.0 - _alpha);
}
}

求解约束 SolveConstrains()

接下来就要对约束进行求解了,遍历每个约束然后计算求解即可,各个求解器可能会在该函数中做一些其他运算,所以定义为虚函数。

virtual void SolveConstrains() {
for (int constrainIndex = 0; constraintIndex < constraintNumber; constraintIndex++) {
_dataInterface->GetRegularConstrain(constrainIndex)->SolveConstrain(_dataInterface, _jointPositions, _jointRotations, _deltaT);
}
}

在函数中我们还为Constrain提供了数据的接口_dataInterface,这是因为约束需要对包括joint的位置和旋转等数据进行读写操作,所以我们需要将接口交给它(类似权限交接,暂时给予)。

求解碰撞约束 SolveCollisions()

碰撞也是一类约束,所以操作和约束求解一样,遍历每个约束然后计算求解即可,各个求解器可能会在该函数中做一些其他运算,所以定义为虚函数。

virtual void SolveCollisions() {
for (int constrainIndex = 0; constraintIndex < constraintNumber; constraintIndex++) {
_dataInterface->GetCollisionConstrain(constrainIndex)->SolveConstrain(_dataInterface, _jointPositions, _jointRotations, _deltaT);
}
}

更新subStep相关数据 UpdateBySubStep()

TODO.... 基类中无实现,定义为虚函数在子类中重写,但是因为不是必须环节,所以不是纯虚函数。

void UpdateBySubStep() {}

更新关节的变换 UpdateJointTransforms()

之前的计算都只算出了新的joint位置和旋转,而并没有计算出最终的变换,我们在本函数中进行Transform的更新。

这里主要注意一点,在Solver的结算过程中,为了让求解过程更加简化和自由,我们并没有考虑各个节点之间的连接关系,所以在这个函数中很重要的一个点就是维持各个节点之间的连接关系,具体请看代码的实现。

// 首先用一个新变量将之前的求解结果全部存储下来
vector<Transform> newJointTransforms(_dataInterface->GetJointNumber(), Transform::Identity);
for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
newJointTransforms[jointIndex].SetTranslation(_jointPositions[jointIndex]);
newJointTransforms[jointIndex].SetRotation(_jointRotations[jointIndex]);
}

// 接下来开始恢复骨架节点之间的连接关系
// 首先以关节中的链为单位,定位到链结构中的骨骼节点
int chainNumber = _dataInterface->GetChainNumber();
for (int chainIndex = 0; chainIndex < chainNumber; chainIndex++) {
int chainLength = _dataInterface->GetChainLength(chainIndex);
for (int chainNodeIndex = 0; chainNodeIndex < chainLength; chainNodeIndex++) {
int jointIndex = _dataInterface->GetChainNodeIndex(chainIndex, chainNodeIndex);
// 接下来找到定位到的节点的子节点(如果该节点是多个链的根节点怎么办?)
int childIndex = _dataInterface->GetChild(jointIndex);
Transform jointTransform = _dataInterface->GetJointTransform(jointIndex);

// 如果joint已经是叶节点了,没有需要恢复的东西,开始其他链的修正
if (childIndex == -1)
continue;

// 找到子节点的初始变换
Transform childTransform = _dataInterface->GetJointTransform(childIndex);

// 找到更新后,joint-child链的旋转关系
VECTOR vecBefore = chileTransform.GetTranslation() - jointTransform.GetTranslation();
VECTOR vecAfter = newJointTransform[childIndex].getTranslation() - newJointTransform[jointIndex].getTranslation();
QUAT parentRotation = QUAT::FindBetween(vecBefore, vecAfter);

// 更新组件空间的旋转,恢复链接关系
jointTransform.setRotation(parentRotation * newJointTransform[jointIndex].getRotation());
jointTransform.setTranslation(newJointTransform[jointIndex].GetTranslation());

// 应用更新
newJointTransform[jointIndex] = jointTransform;
}
}

// 将newJointTransform中的信息更新到Simulation中的信息里面去
for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
Transform jointTransform = newJointTransform[jointIndex];
_dataInterface->SetJointTransform(jointIndex, jointTransform);
// 用作下一步迭代的输入
_dataInterface->SetInputJointTransform(jointIndex, jointTransform);
}

到这里需要的功能基本都实现了,接下来使用一个Solve()来包装前边的内容,让求解的流程有一个更清晰的逻辑链。

求解 Solve()

基本的逻辑如下:

void Solve() {
InitTransform();
ResetConstrains();
SolveImpl();

for (int i = 0; i < _subStep; i++) {
// 终于到了,alpha的更新公式
_alpha = (i + 1.0f) / _subStep;
PrepareSubStep();
SolveConstrains();
SolveCollisions();
}

if (_subStep > 1)
UpdateSubStep();

UpdateJointTransforms();
}
- + \ No newline at end of file diff --git a/docs/joint_solver-series/kawaii.html b/docs/joint_solver-series/kawaii.html index d12b286..bfffe4c 100644 --- a/docs/joint_solver-series/kawaii.html +++ b/docs/joint_solver-series/kawaii.html @@ -9,7 +9,7 @@ - + @@ -17,7 +17,7 @@

Kawaii Joint Solver

Kawaii的网址:link

本文中的Kawaii Joint Solver为上一章节Joint Solver 整体框架中提到的类(我们将其命名为JointSolverBase)的子类。

class KawaiiJointSolver : public JointSolverVase { ... }

Kawaii本质上是一种利用父关节带动子关节运动的一种dynamics,结果会叠加在原动画上。

成员变量

首先我们可以先将Kawaii求解过程中需要的骨骼节点数据打包成一个结构体,方便后面的讲解:

struct KawaiiBoneData {
int boneIndex = -1;
int parentIndex = -1;
VECTOR3 location;
VECTOR3 preLocation;
VECTOR3 poseLocation;
KawaiiJointAttr* jointAttr;
}

其中需要注意,其中的locationpreLocation指的是仿真得到的数据,poseLocation是动画的数据。要注意区分仿真动画。 然后jointAttr是一个指向Kawaii求解器所管理的所有joint的相关属性,这些属性一般是由用户设定的,我们使用jointAttr这个指针来访问它。他一般包括下面这些内容:

struct KawaiiJointAttr {
float damping;
float worldLocDamping;
float worldRotDamping;
float stiffness;
float airflowDamping;
float limitAngle;
}

KawaiiJointSolver的成员变量包括:

    array<KawaiiBoneData>   _kawaiiBones;
VECTOR3 _compMoveVector;
QUAT _compMoveRotation;
Transform _preCompTransform;
float _exponent;
float _teleportDisThreshold; // 根节点的位移上限
float _teleportRotThreshold; // 根节点的旋转上限
float _windSpeed;
VECTOR3 _windDirection;
VECTOR3 _gravity;

// 基类中的成员变量:
// vector<VECTOR3> _jointPositions;
// vector<QUAT> _jointRotations;
// int _subStep;
// float _deltaT;
// float _alpha;
// DataInterface* _dataInterface;

成员函数

为了更好的实现功能,Kawaii求解器中定义了一些功能函数,我们首先对它们进行一些介绍。之后再来看看基类中的虚函数是怎么被实现的。

初始化骨骼节点的数据 InitBoneData()

之前我们将所有的骨骼节点的数据都封装进了一个结构体KawaiiBoneData中,所以首先我们当然需要对其进行一次初始化。

for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
int parentIndex = _dataInterface->GetJointParent(jointIndex);
KawaiiBoneData newBone;
newBone.boneIndex = jointIndex;
Transform boneTransform = _dataInterface->GetJointTransform(jointIndex);
newBone.location = boneTransform.GetLocation();
newBone.poseLocation = newBone.location;
newBone.preLocation = newBone.location;
newBone.jointAttr = (KawaiiJointAttr*)_dataInterface->GetJointAttr(jointIndex);
if (parentIndex < 0) {
// 如果不存在父节点
newBone.parentIndex = -1;
} else {
newBone.parentIndex = parentIndex;
}
_kawaiiBones.add(newBone);
}

修正根节点过大的位移和旋转 UpdateMovement(Transform& transform)

为了让整个模型更加稳定,我们会人为约束两帧之间根节点的位移和旋转,这个约束的阈值就是成员变量中的_teleportDisThreshold_teleportRotThreshold。 函数的输入transform就是当前根节点的运动信息,用于和_preCompTransform进行比对来找到两帧之间的位移和旋转,_preCompTransform的初始化在函数Init()中。

怎么找到相对的位移和旋转?

知道节点在某个坐标系下,两帧的旋转和位移之后,怎么计算得到两帧之间相对的旋转和位移?后一帧的位移/旋转叠加上前一帧的位移/旋转的逆运动即可。

void UpdateMovement(Transform& transform) {
// InverseTransformPosition 叠加逆位移运动
_compMoveVector = transform.InverseTransformPosition(_preCompTransform.getLocation());
if (_compMoveVector.Squared() > _teleportDisThreshold * _teleportDisThreshold) {
_compMoveVector = VECTOR3(0.0, 0.0, 0.0);
}
// InverseTransformRotation 叠加逆旋转运动
_compMoveRotation = transform.InverseTransformRotation(_preCompTransform.getRotation());
if (_compMoveRotation.GetAngle() > _compMoveRotation * _compMoveRotation) {
_compMoveRotation = QUAT::Identity;
}
// 更新_preCompTransform
// TODO 为什么不用_compMoveVector和_compMoveRotation构成新的_preCompTransform?
_preCompTransform = transform;
}

计算惯性和风力 UpdatePose()

如标题所示,计算惯性和风力并更新,有点类似于XPBD在求解约束之前,要先进行一次x=x+vtx=x+vt

for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
// 首先先将对应骨骼的数据取出来,由于我们是要对数据进行更新的,所以需要用引用
auto& bone = _kawaiiBones[jointIndex];
// 获取动画数据和仿真数据
bone.poseLocation = _dataInterface->GetJointTransform(bone.boneIndex);
bone.location = _dataInterface->GetJointSimTransform(bone.boneIndex);

if (bone.parentIndex < 0) {
// 如果没有父节点,那就让其直接跟随用户k帧的动画
bone.preLocation = bone.location;
bone.location = bone.poseLocation;
continue;
}

// 更新风力和damping的作用
VECTOR3 velocity = (bone.location - bone.preLocation) / _deltaT;
bone.preLocation = bone.location;
velocity *= (1.0f - bone.jointAttr->damping);

VECTOR3 windVelocity = _windSpeed * _windDirection;
velocity += windVelocity;

bone.location += velocity * _deltaT;

// 跟随根节点进行运动
bone.location += _compMoveVector * (1.0 - bone.jointAttr->worldLocDamping);
bone.location += (_compMoveRotation.RotateVector(bone.preLocation)-bone.preLocation) * (1.0 - bone.jointAttr->worldRotDamping);

// 重力
bone.location += 0.5 * _gravity * _deltaT * _deltaT;
}

约束仿真结果和动画之间的角度 AdjustAngle(...)

有时候动画师并不希望使用纯仿真的结果,他们希望动画的一切效果还是以自己k出来的为主,仿真只是锦上添花,所以说我们需要将仿真计算出来的结果进行约束。

void AdjustAngle(float limitAngle, VECTOR3& location, VECTOR3& parentLocation, VECTOR3 poseLocation, VECTOR3 parentPoseLocation) {
VECTOR3 boneDir = (location - parentLocation).Normalized();
VECTOR3 poseDir = (poseLocation - parentPoseLocation).Normalized();
VECTOR3 axis = VECTOR3::CrossProduct(poseDir, boneDir);
float angle = Atan2(axis.Length(), VECTOR3::DotProduct(poseDir, boneDir));
float angleOverLimit = angle - limitAngle;

if (angleOverLimit > 0.0f) {
// 将多余的部分转回去
boneDir = boneDir.RotateAngleAxis(_angleOverLimit, axis);
location = boneDir * (location - parentLocation).Length() + parentLocation;
}
}

接下来就是基类原有框架下的重写部分了。

初始化 Init()

除开基类已有的功能外,Kawaii的初始化中的额外工作就是将所有骨骼节点数据调用InitBoneData()进行初始化。

JointSolverBase::Init();
_kawaiiBones.Empty(); // 先清空
if (_kawaiiBones.Num() == 0) {
InitBoneData();
_preCompTransform = _dataInterface->GetComponentTransform();
}

隐式求解 SolveImpl()

Kawaii的隐式求解的过程大致如下:

  • 首先获取当前的根节点的位移和旋转,如果过大就将其修正
  • 应用惯性、重力和风力
  • 将仿真结果叠加到动画效果上
void SolveImpl() {
Transform componentTransform = _dataInterface->GetComponentTransform();
UpdateMovement(componentTransform);
UpdatePose();

int chainNumber = _dataInterface->GetChainNumber();
for (int chainIndex = 0; chainIndex < chainNumber; chainIndex++) {
int chainLength = _dataInterface->GetChainLength(chainIndex);
for (int chainNodeIndex = 0; chainNodeIndex < chainLength; chainNodeIndex++) {
int jointIndex = _dataInterface->GetChainNodeIndex(chainIndex, chainNodeIndex);
// 首先先将对应骨骼的数据取出来,由于我们是要对数据进行更新的,所以需要用引用
auto& bone = _kawaiiBones[jointIndex];
if (bone.parentIndex < 0)
continue;

auto& parentBone = _kawaiiBones[bone.parentIndex];
VECTOR3 poseLocation = bone.poseLocation;
VECTOR3 parentPoseLocation = parentBone.poseLocation;

// 如果没有仿真,本节点应该在的位置
VECTOR3 idealLocation = parentBone.location + (poseLocation - parentPoseLocation);
// 根据关节的刚性进行位置修正
bone.location += (idealLocation - bone.location) * (1.0 - pow(1.0 - bone.jointAttr->stiffness, _exponent));

// 修正角度
AdjustAngle(bone.jointAttr->limitAngle,
bone.location, parentBone.location,
bone.poseLocation, parentBone.poseLocation);

// 恢复长度
float boneLength = (poseLocation - parentPoseLocation).Length();
bone.location = (bone.location - parentBone.location).Normalized() * boneLength + parentBone.location;
}
}
}

为约束和碰撞求解做准备 PrepareSubStep()

在基类的方法中,对碰撞体的transform进行了插值,这里我们还需要对我们定义的骨骼节点数据进行插值:

virtual void PrepareSubStep() {
JointSolverBase::PrepareSubStep();
for (int jointIndex = 0; jointIndex < JointNumber; jointIndex++) {
auto* bone = &_kawaiiBones[jointIndex];
VECTOR3 targetLocation = bone->location;
VECTOR3 preLocation = bone->preLocation;
VECTOR3 dir = (targetLocation - preLocation) / (float)_subStep;
_jointPositions[jointIndex] += dir;
}
}

求解约束 SolveConstrains()

在约束求解以后,还需要对角度进行一次约束。

void SolveConstrains() {
JointSolverBase::SolveConstrains();
int chainNumber = _dataInterface->GetChainNumber();
for (int chainIndex = 0; chainIndex < chainNumber; chainIndex++) {
int chainLength = _dataInterface->GetChainLength(chainIndex);
for (int chainNodeIndex = 0; chainNodeIndex < chainLength; chainNodeIndex++) {
int jointIndex = _dataInterface->GetChainNodeIndex(chainIndex, chainNodeIndex);
auto& bone = _kawaiiBones[jointIndex];
if (bone.parentIndex < 0)
continue;
int parentIndex = bone.parentIndex;
auto& parentBone = _kawaiiBones[parentIndex];
AdjustAngle(bone.jointAttr->limitAngle,
_jointPositions[jointIndex], _jointPositions[parentIndex],
bone.poseLocation, parentBone.poseLocation);
}
}
}

基于上述所有内容,将对应的函数套用到JointSolverBase的求解框架中去,就可以使用Kawaii Joint Solver进行求解了。

- + \ No newline at end of file diff --git a/docs/math-series/tensor_stuff.html b/docs/math-series/tensor_stuff.html index 8bad670..7994bc0 100644 --- a/docs/math-series/tensor_stuff.html +++ b/docs/math-series/tensor_stuff.html @@ -9,7 +9,7 @@ - + @@ -19,7 +19,7 @@ 形变梯度对位置中的每一个分量进行求导可以得到一个3x3的矩阵,那么完整的偏导就是12个3x3的矩阵的几何,这是一个三维张量,其维度为R3×3×12\mathbb{R}^{3\times3\times12}3ordertensor

这里只展示了四个矩阵,实际上应该有12个

我们也可以将其视作一个由矩阵构成的向量,用我们更熟悉的二维的形式来表示三维张量: 3ordervector

这里只展示了四个矩阵,实际上应该有12个

从张量计算的角度出发,之前的计算公式可以重写为:

Ψx=FxΨF\frac{\partial \Psi}{\partial \bf x}=\frac{\partial \bf F}{\partial \bf x}:\frac{\partial \Psi}{\partial \bf F}

这里的“:”代表张量缩并。

该重写方法仅适用于当前场景

该方法只是一个方便理解和代码实现的一个小技巧,并不是一个定理。据我现在的了解,目前这个结论只能够适用于三维张量和二维矩阵这一个场景中,实际情况需要实际分析,绝对不可以盲目套用!

三维张量缩并(张量形式)

张量缩并是向量点积的推广,向量点积是:

xTy=[x0x1x2]T[y0y1y2]=x0y0+x1y1+x2y2{\bf x}^T{\bf y}= \begin{bmatrix} x_0 \\ x_1 \\ x_2 \end{bmatrix}^T \begin{bmatrix} y_0 \\ y_1 \\ y_2 \end{bmatrix}= x_0y_0+x_1y_1+x_2y_2

矩阵缩并所做的事情也差不多:

A:B=[a00a01a10a11]:[b00b01b10b11]=a00b00+a01b01+a10b10+a11b11{\bf A}:{\bf B}= \begin{bmatrix} a_{00} & a_{01} \\ a_{10} & a_{11} \end{bmatrix}: \begin{bmatrix} b_{00} & b_{01} \\ b_{10} & b_{11} \end{bmatrix}= a_{00}b_{00}+a_{01}b_{01}+a_{10}b_{10}+a_{11}b_{11}

我们再进一步扩展到张量:

A:B=[[a0a1a2a3][a4a5a6a7][a8a9a10a11]]:[b0b1b2b3]=[a0b0+a1b1+a2b2+a3b3a4b0+a5b1+a6b2+a7b3a8b0+a9b1+a10b2+a11b3]{\bf A}:{\bf B}= \begin{bmatrix} \begin{bmatrix} a_0 & a_1\\ a_2 & a_3 \end{bmatrix}\\ \\ \begin{bmatrix} a_4 & a_5\\ a_6 & a_7 \end{bmatrix} \\ \\ \begin{bmatrix} a_8 & a_9\\ a_{10} & a_{11} \end{bmatrix} \end{bmatrix}: \begin{bmatrix} b_{0} & b_{1} \\ b_{2} & b_{3} \end{bmatrix}= \begin{bmatrix} a_0b_0+a_1b_1+a_2b_2+a_3b_3\\ a_4b_0+a_5b_1+a_6b_2+a_7b_3\\ a_8b_0+a_9b_1+a_{10}b_2+a_{11}b_3 \end{bmatrix}

flattened

可能这样的计算方法对我们来说还不够容易接受,那么其实不论是几维的张量,我们都可以将其平坦化(flattened,对高维张量来说)或向量化(vectorized,对矩阵来说),然后我们就可以在熟悉的领域去进行计算了。

平坦化和向量化

这里我们引入vec()\text{vec}(\cdot)算子,它能够将将矩阵变换为向量,将任意高阶张量变换为矩阵。首先我们来看看他是怎么向量化矩阵的:

vec(A)=vec([a0a1a2a3])=[a0a2a1a3]\text{vec}({\bf A})=\text{vec}\left( \begin{bmatrix} a_0 & a_1\\ a_2 & a_3 \end{bmatrix} \right)= \begin{bmatrix} a_0\\ a_2 \\ a_1 \\ a_3 \end{bmatrix}

他的逻辑就是:我们将一个矩阵中所有的列按照顺序堆叠起来,其对应的代码是:

static Vector9 flatten (const Matrix3x3& A) {
Vector9 flattened ;
int index = 0;
for (int y = 0; y < 3; y++)
for (int x = 0; x < 3; x++, index++)
flattened [index] = A(x, y);
return flattened;
}

接下来试试将高维张量给平坦化:

vec(A)=vec[[A][B][C]]=[vec(A)vec(B)vec(C)]\text{vec}({\bf \mathbb{A}})=\text{vec}\begin{bmatrix} \begin{bmatrix} {\bf A} \end{bmatrix} \\ \\ \begin{bmatrix} {\bf B} \end{bmatrix}\\ \\ \begin{bmatrix} {\bf C} \end{bmatrix} \end{bmatrix} = \begin{bmatrix} \text{vec}({\bf A}) & \text{vec}({\bf B}) & \text{vec}({\bf C}) \end{bmatrix}

首先我们先将一个三维张量按照之前的思路,将所有列堆叠起来构成单独的一列(我们这里总共就一列),之后再放倒,转换为一行,然后对其中的元素逐一进行向量化:

vec[[a0a1a2a3][a4a5a6a7][a8a9a10a11]]=[vec[a0a1a2a3]vec[a4a5a6a7]vec[a8a9a10a11]]=[a0a4a8a2a6a10a1a5a9a3a7a11]\text{vec} \begin{bmatrix} \begin{bmatrix} a_0 & a_1\\ a_2 & a_3 \end{bmatrix} \\ \\ \begin{bmatrix} a_4 & a_5\\ a_6 & a_7 \end{bmatrix} \\ \\ \begin{bmatrix} a_8 & a_9\\ a_{10} & a_{11} \end{bmatrix} \end{bmatrix} = \begin{bmatrix} \text{vec} \begin{bmatrix} a_0 & a_1\\ a_2 & a_3 \end{bmatrix} & \text{vec} \begin{bmatrix} a_4 & a_5\\ a_6 & a_7 \end{bmatrix} & \text{vec} \begin{bmatrix} a_8 & a_9\\ a_{10} & a_{11} \end{bmatrix} \end{bmatrix}= \begin{bmatrix} a_0 & a_4 &a_8\\ a_2 & a_6 & a_{10}\\ a_1 & a_5 & a_9\\ a_3 & a_7 & a_{11} \end{bmatrix}

三维张量缩并(平坦化形式)

刚刚介绍的一大堆平坦化的东西虽然看起来能够将复杂数据转化为一种更加清晰的形式,但是实际有什么用呢?至少在张量缩并的计算中,我们可以直接给出结论:

A:B=vec(A)Tvec(B){\bf A}:{\bf B} = \text{vec}({\bf A})^T\text{vec}({\bf B})

下面来进行一次推导:

vec(A)Tvec(B)=(vec[[a0a1a2a3][a4a5a6a7][a8a9a10a11]])Tvec[b0b1b2b3]=[a0a4a8a2a6a10a1a5a9a3a7a11]T[b0b2b1b3]=[a0b0+a1b1+a2b2+a3b3a4b0+a5b1+a6b2+a7b3a8b0+a9b1+a10b2+a11b3]=A:B\begin{aligned} \text{vec}({\bf A})^T\text{vec}({\bf B})&= \left( \text{vec} \begin{bmatrix} \begin{bmatrix} a_0 & a_1\\ a_2 & a_3 \end{bmatrix} \\ \\ \begin{bmatrix} a_4 & a_5\\ a_6 & a_7 \end{bmatrix} \\ \\ \begin{bmatrix} a_8 & a_9\\ a_{10} & a_{11} \end{bmatrix} \end{bmatrix} \right)^T \text{vec} \begin{bmatrix} b_0 & b_1 \\ b_2 & b_3 \end{bmatrix} \\ &=\begin{bmatrix} a_0 & a_4 &a_8\\ a_2 & a_6 & a_{10}\\ a_1 & a_5 & a_9\\ a_3 & a_7 & a_{11} \end{bmatrix}^T \begin{bmatrix} b_0 \\ b_2 \\ b_1 \\ b_3 \end{bmatrix}\\ &=\begin{bmatrix} a_0b_0+a_1b_1+a_2b_2+a_3b_3\\ a_4b_0+a_5b_1+a_6b_2+a_7b_3\\ a_8b_0+a_9b_1+a_{10}b_2+a_{11}b_3 \end{bmatrix}\\ &={\bf A}:{\bf B} \end{aligned}
仅可用于三维张量的缩并

据我现在的了解,目前这个结论只能够适用于三维张量和二维矩阵这一个场景中,实际情况需要实际分析,绝对不可以盲目套用!在下面的四维张量的计算中就已经不能适用了,但是flatten的思路是可以使用的。

拓展到力的微分(四维张量)

在隐式积分求解的过程中需要计算能量密度的hessian,也就是力的微分取负,其公式如下:

2Ψx2=FxT2ΨF2Fx\frac{\partial^2\Psi}{\partial {\bf x}^2} = \frac{\partial {\bf F}}{\partial {\bf x}}^T\frac{\partial^2 \Psi}{\partial {\bf F}^2}\frac{\partial {\bf F}}{\partial {\bf x}}

这里面需要注意的是2ΨF2\frac{\partial^2 \Psi}{\partial {\bf F}^2}这一项,能量密度对形变梯度求一次导可以得到一个3x3的矩阵,再求一次导就是一个3x3x3x3的四维张量了。但是实际上和三维张量也没有什么区别,它的平坦化为:

A=[[a0a1a2a3][a4a5a6a7][a8a9a10a11][a12a13a14a15]]=[[A00][A01][A10][A11]]{\bf A} = \begin{bmatrix} \begin{bmatrix} a_0 & a_1\\ a_2 & a_3 \end{bmatrix} & \begin{bmatrix} a_4 & a_5\\ a_6 & a_7 \end{bmatrix} \\ \\ \begin{bmatrix} a_8 & a_9\\ a_{10} & a_{11} \end{bmatrix} & \begin{bmatrix} a_{12} & a_{13}\\ a_{14} & a_{15} \end{bmatrix} \end{bmatrix} = \begin{bmatrix} [{\bf A}_{00}] & [{\bf A}_{01}]\\ [{\bf A}_{10}] & [{\bf A}_{11}] \end{bmatrix}
vec(A)=[vec(A00)vec(A10)vec(A01)vec(A11)]=[a0a8a4a12a2a10a6a14a1a9a5a13a3a11a7a15]\begin{aligned} \text{vec}({\bf A}) &= \begin{bmatrix} \text{vec}({\bf A}_{00}) & \text{vec}({\bf A}_{10}) & \text{vec}({\bf A}_{01}) & \text{vec}({\bf A}_{11}) \end{bmatrix}\\ & = \begin{bmatrix} a_0 & a_8 & a_4 & a_{12}\\ a_2 & a_{10} & a_6 & a_{14}\\ a_1 & a_9 & a_5 & a_{13} \\ a_3 & a_{11} & a_7 & a_{15} \end{bmatrix} \end{aligned}

在计算hessian的情况下,使用平坦化之后的张量进行计算的公式是:

2Ψx2=vec(Fx)Tvec(2ΨF2)vec(Fx)\frac{\partial^2\Psi}{\partial {\bf x}^2} = \text{vec}\left(\frac{\partial {\bf F}}{\partial {\bf x}}\right)^T\text{vec}\left(\frac{\partial^2 \Psi}{\partial {\bf F}^2}\right)\text{vec}\left(\frac{\partial {\bf F}}{\partial {\bf x}}\right)
- + \ No newline at end of file diff --git a/docs/pbd-series/pbd-xpbd-framework.html b/docs/pbd-series/pbd-xpbd-framework.html index 7517c30..7d424d0 100644 --- a/docs/pbd-series/pbd-xpbd-framework.html +++ b/docs/pbd-series/pbd-xpbd-framework.html @@ -9,13 +9,13 @@ - +

PBD, XPBD仿真框架

intro...

PBD

ω˙i=Ii1(τi(ωi×(Iiωi)))\dot{\omega}_i={\bf I}_i^{-1}(\tau_i-(\omega_i\times ({\bf I}_i\omega_i)))

XPBD

- + \ No newline at end of file diff --git a/docs/unity-series/CollisionIntro.html b/docs/unity-series/CollisionIntro.html index e6cb5d8..0a4771a 100644 --- a/docs/unity-series/CollisionIntro.html +++ b/docs/unity-series/CollisionIntro.html @@ -9,14 +9,14 @@ - +

碰撞系统介绍

碰撞体 Collider

Unity中使用碰撞体来表达物理碰撞的计算中,Object的具体形状。碰撞体是不可见的,并且也不需要和游戏中Object的网格体具有一样的形状。

Unity下的碰撞体有以下几种:

  • Primitive Collider:最简单的碰撞体,3D情况下,Unity为用户提供了Box、Sphere和Capsule三种;
  • Compound Collider:由多个Primitive碰撞体构成的复合碰撞体,一般用于Primitive Collider无法近似Object形状的情况,仅适用于Rigidbody component,并且碰撞体需要放在GameObject的root层级;
  • Mesh Collider:网格体碰撞体,会带来比较大的开销,需要谨慎使用。

碰撞体之间的交互方式主要由Rigidbody component的设置来控制,主要可以设置为以下三类:

  • Static Collider:静态碰撞体,是一种具有碰撞体但是没有Rigid component的GameObject。一般用于场景中不会运动的物体,例如墙壁和地面等,静态碰撞体可以与动态碰撞体产生交互,但是静态碰撞体本身并不会受到碰撞响应的影响;
  • Rigidbody Collider:动态碰撞体,与静态碰撞体相对,是一种带有没有开启kinematic的Rigidbody组件的GameObject,会受到碰撞响应的影响,其行为完全由物理引擎接管,是最常用的碰撞体;
  • Kinematic Rigidbody Collider:是一种带有开启kinematic的Rigidbody组件的GameObject,这一类碰撞体不像Rigidbody Collider一样会对碰撞或者力有响应,而是使用脚本计算Transform Component来控制运动。通过改变Rigidbody Component中变量IsKinematic的值,可以让碰撞体在Rigidbody Collider和Kinematic Rigidbody Collider之间切换,一个常见的例子是布娃娃,正常情况下角色的肢体根据预设动画正常移动,当遇到碰撞或爆炸时,关闭所有肢体的IsKinematic,角色将被表现为一个physics object,以一个比较自然的动作被抛飞。

物理材质 Physics material

用于定义碰撞过程中,碰撞表面的一些行为,主要包括摩擦和弹性(反弹的力,不会发生形变)。

触发器 Trigger

用于触发一些碰撞事件,通过trigger object脚本中的OnTriggerEnter来定义。

碰撞回调函数

当碰撞第一次被发现的时候,会触发OnCollisionEnter函数;在“碰撞被检测到”到“碰撞对被分离”之间的过程中(可能会有好几帧),会触发OnCollisionStay函数;当碰撞对被分离,会触发OnCollisionExit函数。

Trigger可以调用类似的OnTriggerEnterOnTriggerStayOnTriggerExit函数。

这些回调函数的更多细节和用例在MonoBehaviour

注意事项

对于非trigger的碰撞,如果碰撞对中所有object都开启了动力学(IsKinematic = true),那么这些回调函数不会被调用。这样设计也合理,因为总需要一个东西来触发碰撞体的脚本。

碰撞行为表

当两个对象发生碰撞时,根据碰撞对象刚体的配置,可能会发生许多不同的脚本事件。 下图给出了根据附加到对象的组件调用哪些事件函数的详细信息。 有些组合只会导致两个对象之一受到碰撞影响,但一般规则是物理不会应用于未附加 Rigidbody 组件的对象。 collisionmatrix

- + \ No newline at end of file diff --git a/docs/unreal-series/bone-anim.html b/docs/unreal-series/bone-anim.html index 59c6bcb..9af6370 100644 --- a/docs/unreal-series/bone-anim.html +++ b/docs/unreal-series/bone-anim.html @@ -9,13 +9,13 @@ - +

骨骼动画

参考资料:

UE4/UE5 动画的原理和性能优化:link

骨骼动画的思想

一个Mesh想要动起来,那么就需要去对每个顶点做Transform(位移/旋转/缩放),那么每一帧都存这么多Transform,1秒24帧(或更多),一整段动画要存很多数据量,所以就有了骨骼这个概念。

骨骼这个概念,本质上就是压缩相同顶点的Transform的一种方式。具体来说,就是把Mesh上一部分的顶点和其中一个或多个骨骼做绑定,那么我们只要记录这个骨骼的Transform就好了。Mesh上的顶点会有对应骨骼的weight,每一帧只要将对应的骨骼的Transform做一个加权求和就能够得到该顶点的Transform。

所以整个动画分成两个阶段:

  1. 现在游戏线程中的TickComponent里面求得当前帧的Pose(Pose:每个骨骼的Transform)
  2. 渲染线程中根据最终Pose做CPUSkin或GPUSkin算出顶点信息,并进行绘制

先骨骼,后render mesh(skinned mesh)。

TickComponent

TickComponent是UActorComponent类的成员函数,该函数会在每一帧被调用,以计算对应的组件在这一帧中的行为。

Game Thread

Game Thread

- + \ No newline at end of file diff --git a/docs/unreal-series/bounds.html b/docs/unreal-series/bounds.html index 8aae71d..8f565ab 100644 --- a/docs/unreal-series/bounds.html +++ b/docs/unreal-series/bounds.html @@ -9,7 +9,7 @@ - + @@ -22,7 +22,7 @@ useBoundsFromMasterPoseComponent

  • 所在类:USkinnedMeshComponent
  • 路径:Engine\Source\Runtime\Engine\Classes\Components\SkinnedMeshComponent.h
  • 用途:如果标记为true,会在USkinnedMeshComponent::CalcMeshBound计算SkinnedMesh组件的Bound时候使用MasterPoseComponentInst的Bound
  • bSkipBoundsUpdateWhenInterpolating bSkipBoundsUpdateWhenInterpolating

    • 所在类:USkeletalMeshComponent
    • 路径:Engine\Source\Runtime\Engine\Classes\Components\SkeletalMeshComponent.h
    • 用途:如果设置为true,那么只会在tick计算完成后更新Bound,中间插值的时候就不更新了
  • bComponentUseFixedSkelBounds bComponentUseFixedSkelBounds

    • 所在类:USkinnedMeshComponent
    • 路径:Engine\Source\Runtime\Engine\Classes\Components\SkinnedMeshComponent.h
    • 用途:如果设置为true,那么该组件就会使用一个固定的Bound。这会带来两个影响:1)Bound不会根据Mesh的旋转等操作更新,减少计算量;2)计算得到的Bound会比自动的要大,这是为了确保Mesh能够被包住
  • - + \ No newline at end of file diff --git a/docs/unreal-series/resource.html b/docs/unreal-series/resource.html index 2bb2e42..d876fc4 100644 --- a/docs/unreal-series/resource.html +++ b/docs/unreal-series/resource.html @@ -9,13 +9,13 @@ - +
    - + \ No newline at end of file diff --git a/index.html b/index.html index bdb0445..e62a3db 100644 --- a/index.html +++ b/index.html @@ -9,13 +9,13 @@ - +

    Ryao's Blog

    少一些判断,多一些旁观和理解,多挑战自己的认知盲区而不是道德边界

    Focus on What Matters

    你好!👋

    我是Ryao,目前在上海交通大学控制科学与工程专业攻读硕士学位。
    我目前主要研究方向是计算机图形学(模拟、渲染)和数字孪生建模。
    我的邮箱:lilkotyo@gmail.com, lil-kotyo@sjtu.edu.cn.
    如果你对图形学感兴趣,欢迎联系我!

    Powered by React

    Hi there 👋

    I'm Ryao, a master student in control engineering.
    I'm currently working on Computer Graphics (simulation, rendering) and Digital Twins Modeling.
    How to reach me: lilkotyo@gmail.com or lil-kotyo@sjtu.edu.cn.
    Contact me if you are interested in graphics and have related topics to discuss!

    - + \ No newline at end of file diff --git a/markdown-page.html b/markdown-page.html index 4e84413..b25927b 100644 --- a/markdown-page.html +++ b/markdown-page.html @@ -9,13 +9,13 @@ - +

    Markdown page example

    You don't need React to write simple standalone pages.

    - + \ No newline at end of file