diff --git a/404.html b/404.html index 64501f1..18610d6 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/js/8c4028e7.25fb4b27.js b/assets/js/8c4028e7.25fb4b27.js new file mode 100644 index 0000000..ffd0cb0 --- /dev/null +++ b/assets/js/8c4028e7.25fb4b27.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkryao_blog=self.webpackChunkryao_blog||[]).push([[9293],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>k});var a=n(7294);function l(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(l[n]=e[n]);return l}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(l[n]=e[n])}return l}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},u=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,l=e.mdxType,r=e.originalType,p=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),c=s(n),m=l,k=c["".concat(p,".").concat(m)]||c[m]||d[m]||r;return n?a.createElement(k,o(o({ref:t},u),{},{components:n})):a.createElement(k,o({ref:t},u))}));function k(e,t){var n=arguments,l=t&&t.mdxType;if("string"==typeof e||l){var r=n.length,o=new Array(r);o[0]=m;var i={};for(var p in t)hasOwnProperty.call(t,p)&&(i[p]=t[p]);i.originalType=e,i[c]="string"==typeof e?e:l,o[1]=i;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>i,toc:()=>s});var a=n(7462),l=(n(7294),n(3905));const r={sidebar_position:5},o="\u52a8\u753b\u8282\u70b9",i={unversionedId:"unreal-series/animnode",id:"unreal-series/animnode",title:"\u52a8\u753b\u8282\u70b9",description:"\u52a8\u753b\u8282\u70b9\u5728\u52a8\u753b\u84dd\u56fe\u4e2d\u7528\u4e8e\u6267\u884c\u591a\u79cd\u64cd\u4f5c\uff0c\u4f8b\u5982\u5904\u7406\u52a8\u753b Pose\u3001\u6df7\u5408\u52a8\u753b\u59ff\u52bf\u4ee5\u53ca\u64cd\u63a7\u9aa8\u9abc\u7f51\u683c\u4f53\u7684\u9aa8\u9abc\u3002",source:"@site/docs/unreal-series/animnode.md",sourceDirName:"unreal-series",slug:"/unreal-series/animnode",permalink:"/docs/unreal-series/animnode",draft:!1,editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/unreal-series/animnode.md",tags:[],version:"current",sidebarPosition:5,frontMatter:{sidebar_position:5},sidebar:"tutorialSidebar",previous:{title:"UE\u7684\u78b0\u649e\u4e0e\u68c0\u6d4b",permalink:"/docs/unreal-series/collision"}},p={},s=[{value:"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6",id:"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6",level:2},{value:"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6",id:"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6",level:2},{value:"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c",id:"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c",level:2}],u={toc:s},c="wrapper";function d(e){let{components:t,...r}=e;return(0,l.kt)(c,(0,a.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,l.kt)("h1",{id:"\u52a8\u753b\u8282\u70b9"},"\u52a8\u753b\u8282\u70b9"),(0,l.kt)("p",null,"\u52a8\u753b\u8282\u70b9\u5728\u52a8\u753b\u84dd\u56fe\u4e2d\u7528\u4e8e\u6267\u884c\u591a\u79cd\u64cd\u4f5c\uff0c\u4f8b\u5982\u5904\u7406\u52a8\u753b Pose\u3001\u6df7\u5408\u52a8\u753b\u59ff\u52bf\u4ee5\u53ca\u64cd\u63a7\u9aa8\u9abc\u7f51\u683c\u4f53\u7684\u9aa8\u9abc\u3002"),(0,l.kt)("admonition",{title:"\u52a8\u753b\u84dd\u56fe",type:"tip"},(0,l.kt)("p",{parentName:"admonition"},"\u52a8\u753b\u84dd\u56fe\u662f\u52a8\u753b\u5b9e\u4f8b\uff08AnimInstance\uff09\u7684\u5b50\u7c7b\u3002\u53ef\u4ee5\u8fd9\u4e48\u7406\u89e3\uff0c\u52a8\u753b\u84dd\u56fe\u5c31\u662f\u52a8\u753b\u5b9e\u4f8b\u7684\u53ef\u89c6\u5316\u811a\u672c\u3002\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u7f16\u8f91\u52a8\u753b\u84dd\u56fe\u6765\u7f16\u5199\u52a8\u753b\u5b9e\u4f8b\u7684\u903b\u8f91\u3002")),(0,l.kt)("p",null,"\u4e00\u4e2a\u5b8c\u6574\u7684\u52a8\u753b\u8282\u70b9\u5305\u62ec\u4e24\u4e2a\u57fa\u672c\u7ec4\u4ef6\uff1a"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"\u4e00\u4e2a\u8fd0\u884c\u65f6\u7ed3\u6784\u4f53\uff08AnimNode\uff09\uff0c\u7528\u4e8e\u6267\u884c\u751f\u6210\u8f93\u51fa Pose \u6240\u9700\u7684\u5b9e\u9645\u8ba1\u7b97"),(0,l.kt)("li",{parentName:"ul"},"\u4e00\u4e2a\u7f16\u8f91\u5668\u5bb9\u5668\u7c7b\uff08AnimGraphNode\uff09\uff0c\u8d1f\u8d23\u5904\u7406\u56fe\u6807\u8282\u70b9\u7684\u89c6\u89c9\u8868\u73b0\u548c\u529f\u80fd\uff0c\u4f8b\u5982\u8282\u70b9\u6807\u9898\u548c\u4e0a\u4e0b\u6587\u83dc\u5355")),(0,l.kt)("h2",{id:"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6"},"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6"),(0,l.kt)("p",null,"\u8fd0\u884c\u65f6\u7ed3\u6784\u4f53\u662f\u4e00\u79cd",(0,l.kt)("strong",{parentName:"p"},"\u7ed3\u6784\u4f53"),"\uff0c\u6d3e\u751f\u81ea FAnimNode_Base \u7c7b\uff0c\u8d1f\u8d23\u521d\u59cb\u5316\u3001\u66f4\u65b0\u4ee5\u53ca\u5728\u4e00\u4e2a\u6216\u591a\u4e2a\u8f93\u5165\u59ff\u52bf\u4e0a\u6267\u884c\u64cd\u4f5c\u6765\u751f\u6210\u6240\u9700\u7684\u8f93\u51fa\u59ff\u52bf\u3002\u5b83\u8fd8\u4f1a\u58f0\u660e\u8282\u70b9\u4e3a\u6267\u884c\u6240\u9700\u64cd\u4f5c\u9700\u5177\u5907\u7684\u8f93\u5165\u59ff\u52bf\u94fe\u63a5\u548c\u5c5e\u6027\u3002"),(0,l.kt)("p",null,"\u4e00\u822c\u6765\u8bf4\u9700\u8981\uff1a"),(0,l.kt)("ol",null,(0,l.kt)("li",{parentName:"ol"},"Pose \u8f93\u5165")),(0,l.kt)("p",null,"Pose \u8f93\u5165\u4e00\u822c\u662f\u901a\u8fc7\u521b\u5efa FPoseLink \u6216 FComponentSpacePoseLink \u7c7b\u578b\u7684\u5c5e\u6027\u6765\u516c\u5f00\u4e3a\u4e00\u4e2aPin\u3002\u5176\u4e2d FPoseLink \u7528\u4e8e\u5904\u7406 Local Space \u7684 Pose \u65f6\u4f7f\u7528\uff0c\u4f8b\u5982\u6df7\u5408\u52a8\u753b\u3002FComponentSpacePoseLink \u5728\u5904\u7406 Component Space \u4e2d\u7684 Pose \u65f6\u4f7f\u7528\u3002\u4f8b\u5982\uff1a"),(0,l.kt)("center",null,(0,l.kt)("p",null,(0,l.kt)("img",{alt:"animnode",src:n(6807).Z,width:"993",height:"657"}))),(0,l.kt)("p",null,"\u4e00\u4e2a\u8282\u70b9\u8fd8\u53ef\u4ee5\u6709\u591a\u4e2a Pose \u8f93\u5165\u3002\u53e6\u5916\uff0c\u8fd9\u4e24\u79cd\u7c7b\u578b\u7684\u5c5e\u6027\u53ea\u80fd\u516c\u5f00\u4e3a\u8f93\u5165\u5f15\u811a\uff0c\u65e0\u6cd5\u88ab\u9690\u85cf\u6216\u8005\u4ec5\u4f5c\u4e3a Details \u9762\u677f\u4e2d\u7684\u53ef\u7f16\u8f91\u5c5e\u6027\u3002"),(0,l.kt)("ol",{start:2},(0,l.kt)("li",{parentName:"ol"},"\u5c5e\u6027\u548c\u6570\u636e\u8f93\u5165")),(0,l.kt)("p",null,"\u53ef\u4ee5\u901a\u8fc7 UPROPERTY \u5b8f\u6765\u58f0\u660e\u81ea\u5b9a\u4e49\u5c5e\u6027\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-cpp"},"UPROPERTY(Category=Settings, meta(PinShownByDefault))\nmutable float Alpha;\n")),(0,l.kt)("p",null,"\u4f7f\u7528\u7279\u6b8a\u7684 meta\uff0c\u52a8\u753b\u8282\u70b9\u5c5e\u6027\u53ef\u4ee5\u516c\u5f00\u4e3a\u6570\u636e\u8f93\u5165\u5f15\u811a\uff0c\u4ee5\u5141\u8bb8\u503c\u4f20\u9012\u5230\u8282\u70b9\uff1a"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"NeverAsPin\uff1a\u5c06\u5c5e\u6027\u4f5c\u4e3aAnimGraph\u4e2d\u7684\u6570\u636e\u5f15\u811a\u9690\u85cf\uff0c\u5e76\u4e14\u4ec5\u53ef\u5728\u8282\u70b9\u7684 \u7ec6\u8282\uff08Details\uff09 \u9762\u677f\u4e2d\u7f16\u8f91"),(0,l.kt)("li",{parentName:"ul"},"PinHiddenByDefault\uff1a\u5c06\u5c5e\u6027\u4f5c\u4e3a\u5f15\u811a\u9690\u85cf\u3002\u4f46\u662f\u53ef\u4ee5\u901a\u8fc7 Details \u9762\u677f\u8bbe\u7f6e\uff0c\u5c06\u5c5e\u6027\u4f5c\u4e3a\u6570\u636e\u5f15\u811a\u5728AnimGraph\u4e2d\u516c\u5f00"),(0,l.kt)("li",{parentName:"ul"},"PinShownByDefault\uff1a\u5c06\u5c5e\u6027\u4f5c\u4e3a\u6570\u636e\u5f15\u811a\u5728AnimGraph\u4e2d\u516c\u5f00"),(0,l.kt)("li",{parentName:"ul"},"AlwaysAsPin\uff1a\u59cb\u7ec8\u5c06\u5c5e\u6027\u4f5c\u4e3a\u6570\u636e\u70b9\u5728AnimGraph\u4e2d\u516c\u5f00")),(0,l.kt)("h2",{id:"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6"},"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6"),(0,l.kt)("p",null,"\u7f16\u8f91\u5668\u7c7b\u6d3e\u751f\u81ea UAnimGraphNode_Base\uff0c\u8d1f\u8d23\u8282\u70b9\u6807\u9898\u7b49\u89c6\u89c9\u5143\u7d20\u6216\u6dfb\u52a0\u4e0a\u4e0b\u6587\u83dc\u5355\u64cd\u4f5c\u3002\u7f16\u8f91\u5668\u7c7b\u5e94\u8be5\u5305\u542b\u4e00\u4e2a\u516c\u5f00\u4e3a\u53ef\u7f16\u8f91\u7684\u8fd0\u884c\u65f6\u8282\u70b9\u5b9e\u4f8b\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-cpp"},"UPROPERTY(Category=Settings)\nFAnimNode_ApplyAdditive Node;\n")),(0,l.kt)("h2",{id:"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c"},"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c"),(0,l.kt)("p",null,"\u52a8\u753b\u84dd\u56fe\u662f\u4ee5\u8282\u70b9\u6811\u7684\u65b9\u5f0f\u6765\u7ec4\u7ec7\u52a8\u753b\u7684\u8fd0\u884c\u903b\u8f91\u7684\u3002\u5728\u8fd0\u4f5c\u7684\u8fc7\u7a0b\u4e2d\uff0c\u4e3b\u8981\u9700\u8981\u4e86\u89e3\u4e09\u4e2a\u540d\u8bcd\uff1a"),(0,l.kt)("ol",null,(0,l.kt)("li",{parentName:"ol"},"\u66f4\u65b0\uff08Update\uff09")),(0,l.kt)("p",null,"\u8282\u70b9\u7684Update\u7528\u4e8e\u6839\u636eWeight\u8ba1\u7b97\u52a8\u753b\u7684\u5404\u79cd\u6743\u91cd\u3002\u56e0\u4e3aWeight\u4f1a\u5728\u4e0b\u4e00\u9636\u6bb5\u6e05\u7a7a\u3002"),(0,l.kt)("p",null,"\u5982\u679c\u6309\u7167Epic\u7684\u7f16\u5199\u4e60\u60ef\uff0c\u6211\u4eec\u5e94\u8be5\u5728Update\u91cc\u9762\u62ff\u5230\u6240\u6709\u5916\u90e8\u6570\u636e\u5e76\u4e14\u9884\u8ba1\u7b97\uff0c\u4fdd\u8bc1Evaluate\u53ef\u4ee5\u76f4\u63a5\u4f7f\u7528\u3002"),(0,l.kt)("ol",{start:2},(0,l.kt)("li",{parentName:"ol"},"\u8bc4\u4f30\uff08Evaluate\uff09")),(0,l.kt)("p",null,"\u6839\u636e\u4e0a\u4e00\u4e2a\u8282\u70b9\u7684Pose\uff0c\u8ba1\u7b97\u51fa\u8f93\u51fa\u5230\u4e0b\u4e2a\u8282\u70b9\u7684Pose\u3002"),(0,l.kt)("p",null,"\u8fd9\u662f\u52a8\u753b\u8282\u70b9\u6700\u91cd\u8981\u7684\u90e8\u5206\u3002\u6b63\u5e38\u6765\u8bf4\u6211\u4eec\u5e94\u8be5\u628a\u9aa8\u9abc\u8ba1\u7b97\u90e8\u5206\u90fd\u653e\u5728\u8fd9\u91cc\u3002"),(0,l.kt)("p",null,"\u6ce8\u610fUpdate\u548cEvaluate\u90fd\u6709\u53ef\u80fd\u8fd0\u884c\u5728\u5b50\u7ebf\u7a0b\u4e0a\uff0c\u9664\u4e86\u8bfb\u5199AnimInstanceProxy\u5916\uff0c\u64cd\u4f5c\u5176\u4ed6\u4e1c\u897f\u90fd\u4e0d\u662f\u7ebf\u7a0b\u5b89\u5168\u7684\uff0c\u5c3d\u53ef\u80fd\u4e0d\u8981\u78b0\u5916\u90e8\u7684UObject\u3002"),(0,l.kt)("ol",{start:3},(0,l.kt)("li",{parentName:"ol"},"\u6839\u8282\u70b9\uff08Root\uff09")),(0,l.kt)("p",null,"\u4e5f\u53eb OutPut Pose \u8282\u70b9\u3002\u6839\u8282\u70b9\u662f\u6700\u91cd\u8981\u7684\u8282\u70b9\u3002\u5bf9\u4e8e\u7528\u6237\u6765\u8bf4\uff0c\u4ed6\u662f\u6240\u6709\u52a8\u753b\u903b\u8f91\u7684\u8f93\u51fa\u8282\u70b9\u3002\u4f46\u662f\u5bf9\u4e8e\u84dd\u56fe\u6765\u8bf4\uff0c\u4ed6\u662f\u6574\u4e2a\u84dd\u56fe\u8282\u70b9\u7684\u5f00\u59cb\u3002AnimInstance \u5c06\u4ece\u8fd9\u91cc\u5f00\u59cb\u5efa\u7acb\u6574\u4e2a\u52a8\u753b\u8282\u70b9\u7684\u6811\u72b6\u8054\u7cfb\u3002"),(0,l.kt)("p",null,"\u4ee5\u4e0b\u9762\u7684\u84dd\u56fe\u4e3e\u4f8b\uff1a"),(0,l.kt)("center",null,(0,l.kt)("p",null,(0,l.kt)("img",{alt:"work",src:n(6686).Z,width:"1735",height:"1087"}))),(0,l.kt)("ol",null,(0,l.kt)("li",{parentName:"ol"},"\u5728 Update \u7684\u65f6\u5019\uff0c\u6267\u884c Root \u7684 Update"),(0,l.kt)("li",{parentName:"ol"},"Root \u627e\u5230\u8fde\u63a5\u5230\u4ed6\u7684\u4e0a\u4e00\u4e2a\u8282\u70b9\uff0cBlendPosesByBool \u8282\u70b9\uff0c\u6267\u884c\u4ed6\u7684 Update"),(0,l.kt)("li",{parentName:"ol"},"BlendPosesByBool \u8282\u70b9\u7684 Update \u9996\u5148\u4f1a\u8bfb\u53d6\u6240\u6709 Pin \u7684\u503c\uff0c\u8fd9\u4e2a\u8282\u70b9\u6765\u8bf4\u4e3b\u8981\u662f Active Value"),(0,l.kt)("li",{parentName:"ol"},"\u7136\u540e\u6839\u636e Value\uff0c\u6267\u884c\u5bf9\u5e94\u8282\u70b9\u7684 Update\uff0c\u4e5f\u5c31\u662f\u4e0a\u4e00\u4e2a\u8282\u70b9\uff0cBlendSpace Player\u8282\u70b9\u7684 Update"),(0,l.kt)("li",{parentName:"ol"},"BlendSpacePlayer \u7684 Update \u4f1a\u8bfb\u53d6 Speed \u7684\u503c\u5230\u5c5e\u6027\u91cc"),(0,l.kt)("li",{parentName:"ol"},"\u7136\u540e\u662f Evaluate\uff0c\u6cbf\u7740\u540c\u6837\u7684\u7ebf\u8def"),(0,l.kt)("li",{parentName:"ol"},"\u9996\u5148\u6267\u884c Output \u7684 Evaluate\uff0c\u4ed6\u4f1a\u5148\u6267\u884c\u4e0a\u4e00\u4e2a\u8282\u70b9\u7684 Evaluate"),(0,l.kt)("li",{parentName:"ol"},"\u4e5f\u5c31\u662f BlendPosesByBool \u7684 Evaluate\uff0c\u8fd9\u91cc\u4ed6\u4e5f\u4f1a\u5148\u6267\u884c\u5f53\u524d\u6fc0\u6d3b\u7684\u8282\u70b9\u7684 Evaluate\uff0c\u4e5f\u5c31\u662f BlendSpacePlayer \u7684 Evaluate"),(0,l.kt)("li",{parentName:"ol"},"BlendSpacePlayer \u7684 Evaluate \u5f88\u7b80\u5355\uff0c\u6839\u636e\u8f93\u5165\u7684\u53c2\u6570\uff0c\u4ece BlendSpace \u91cc\u9762\u8f93\u51fa\u4e00\u4e2a\u6df7\u5408\u7684 Pose"),(0,l.kt)("li",{parentName:"ol"},"\u7136\u540e\u56de\u5230 BlendPosesByBool\uff0c\u5982\u679c\u6709\u6df7\u5408\u65f6\u95f4\u800c\u4e14\u5904\u4e8e\u6df7\u5408\u72b6\u6001\uff0c\u4ed6\u4f1a\u628a\u4e24\u4e2a Pose \u6309\u7167\u6bd4\u4f8b\u6df7\u5408\u518d\u8f93\u51fa\uff0c\u5982\u679c\u6ca1\u6709\uff0c\u5219\u76f4\u63a5\u539f\u6837\u8f93\u51fa\u5176\u4e2d\u4e00\u4e2a Pose\uff0c\u5f53\u524d\u662f BlendSpace \u7684\u8f93\u51fa"),(0,l.kt)("li",{parentName:"ol"},"\u6700\u540e\u7684Output\u8282\u70b9\u628a\u524d\u9762\u7684Pose\u8f93\u51fa\uff0c\u5927\u529f\u544a\u6210")))}d.isMDXComponent=!0},6807:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/animnode-8b8172652bbedb6e6a33f62bfbc071c8.jpg"},6686:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/animnodework-b79e79cc4c54cf679ce8cddd4e743f25.jpg"}}]); \ No newline at end of file diff --git a/assets/js/8c4028e7.4e4268a6.js b/assets/js/8c4028e7.4e4268a6.js deleted file mode 100644 index 9f42a09..0000000 --- a/assets/js/8c4028e7.4e4268a6.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkryao_blog=self.webpackChunkryao_blog||[]).push([[9293],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>k});var a=n(7294);function l(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function r(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t=0||(l[n]=e[n]);return l}(e,t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(l[n]=e[n])}return l}var p=a.createContext({}),s=function(e){var t=a.useContext(p),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},u=function(e){var t=s(e.components);return a.createElement(p.Provider,{value:t},e.children)},c="mdxType",d={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},m=a.forwardRef((function(e,t){var n=e.components,l=e.mdxType,r=e.originalType,p=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),c=s(n),m=l,k=c["".concat(p,".").concat(m)]||c[m]||d[m]||r;return n?a.createElement(k,o(o({ref:t},u),{},{components:n})):a.createElement(k,o({ref:t},u))}));function k(e,t){var n=arguments,l=t&&t.mdxType;if("string"==typeof e||l){var r=n.length,o=new Array(r);o[0]=m;var i={};for(var p in t)hasOwnProperty.call(t,p)&&(i[p]=t[p]);i.originalType=e,i[c]="string"==typeof e?e:l,o[1]=i;for(var s=2;s{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>o,default:()=>d,frontMatter:()=>r,metadata:()=>i,toc:()=>s});var a=n(7462),l=(n(7294),n(3905));const r={sidebar_position:5},o="\u52a8\u753b\u8282\u70b9",i={unversionedId:"unreal-series/animnode",id:"unreal-series/animnode",title:"\u52a8\u753b\u8282\u70b9",description:"\u52a8\u753b\u8282\u70b9\u5728\u52a8\u753b\u84dd\u56fe\u4e2d\u7528\u4e8e\u6267\u884c\u591a\u79cd\u64cd\u4f5c\uff0c\u4f8b\u5982\u5904\u7406\u52a8\u753b Pose\u3001\u6df7\u5408\u52a8\u753b\u59ff\u52bf\u4ee5\u53ca\u64cd\u63a7\u9aa8\u9abc\u7f51\u683c\u4f53\u7684\u9aa8\u9abc\u3002",source:"@site/docs/unreal-series/animnode.md",sourceDirName:"unreal-series",slug:"/unreal-series/animnode",permalink:"/docs/unreal-series/animnode",draft:!1,editUrl:"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/docs/unreal-series/animnode.md",tags:[],version:"current",sidebarPosition:5,frontMatter:{sidebar_position:5},sidebar:"tutorialSidebar",previous:{title:"UE\u7684\u78b0\u649e\u4e0e\u68c0\u6d4b",permalink:"/docs/unreal-series/collision"}},p={},s=[{value:"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6",id:"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6",level:2},{value:"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6",id:"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6",level:2},{value:"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c",id:"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c",level:2}],u={toc:s},c="wrapper";function d(e){let{components:t,...r}=e;return(0,l.kt)(c,(0,a.Z)({},u,r,{components:t,mdxType:"MDXLayout"}),(0,l.kt)("h1",{id:"\u52a8\u753b\u8282\u70b9"},"\u52a8\u753b\u8282\u70b9"),(0,l.kt)("p",null,"\u52a8\u753b\u8282\u70b9\u5728\u52a8\u753b\u84dd\u56fe\u4e2d\u7528\u4e8e\u6267\u884c\u591a\u79cd\u64cd\u4f5c\uff0c\u4f8b\u5982\u5904\u7406\u52a8\u753b Pose\u3001\u6df7\u5408\u52a8\u753b\u59ff\u52bf\u4ee5\u53ca\u64cd\u63a7\u9aa8\u9abc\u7f51\u683c\u4f53\u7684\u9aa8\u9abc\u3002"),(0,l.kt)("p",null,"::: \u52a8\u753b\u84dd\u56fe"),(0,l.kt)("p",null,"\u52a8\u753b\u84dd\u56fe\u662f\u52a8\u753b\u5b9e\u4f8b\uff08AnimInstance\uff09\u7684\u5b50\u7c7b\u3002\u53ef\u4ee5\u8fd9\u4e48\u7406\u89e3\uff0c\u52a8\u753b\u84dd\u56fe\u5c31\u662f\u52a8\u753b\u5b9e\u4f8b\u7684\u53ef\u89c6\u5316\u811a\u672c\u3002\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u7f16\u8f91\u52a8\u753b\u84dd\u56fe\u6765\u7f16\u5199\u52a8\u753b\u5b9e\u4f8b\u7684\u903b\u8f91\u3002"),(0,l.kt)("p",null,":::"),(0,l.kt)("p",null,"\u4e00\u4e2a\u5b8c\u6574\u7684\u52a8\u753b\u8282\u70b9\u5305\u62ec\u4e24\u4e2a\u57fa\u672c\u7ec4\u4ef6\uff1a"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"\u4e00\u4e2a\u8fd0\u884c\u65f6\u7ed3\u6784\u4f53\uff08AnimNode\uff09\uff0c\u7528\u4e8e\u6267\u884c\u751f\u6210\u8f93\u51fa Pose \u6240\u9700\u7684\u5b9e\u9645\u8ba1\u7b97"),(0,l.kt)("li",{parentName:"ul"},"\u4e00\u4e2a\u7f16\u8f91\u5668\u5bb9\u5668\u7c7b\uff08AnimGraphNode\uff09\uff0c\u8d1f\u8d23\u5904\u7406\u56fe\u6807\u8282\u70b9\u7684\u89c6\u89c9\u8868\u73b0\u548c\u529f\u80fd\uff0c\u4f8b\u5982\u8282\u70b9\u6807\u9898\u548c\u4e0a\u4e0b\u6587\u83dc\u5355")),(0,l.kt)("h2",{id:"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6"},"\u8fd0\u884c\u65f6\u8282\u70b9\u7ec4\u4ef6"),(0,l.kt)("p",null,"\u8fd0\u884c\u65f6\u7ed3\u6784\u4f53\u662f\u4e00\u79cd",(0,l.kt)("strong",{parentName:"p"},"\u7ed3\u6784\u4f53"),"\uff0c\u6d3e\u751f\u81ea FAnimNode_Base \u7c7b\uff0c\u8d1f\u8d23\u521d\u59cb\u5316\u3001\u66f4\u65b0\u4ee5\u53ca\u5728\u4e00\u4e2a\u6216\u591a\u4e2a\u8f93\u5165\u59ff\u52bf\u4e0a\u6267\u884c\u64cd\u4f5c\u6765\u751f\u6210\u6240\u9700\u7684\u8f93\u51fa\u59ff\u52bf\u3002\u5b83\u8fd8\u4f1a\u58f0\u660e\u8282\u70b9\u4e3a\u6267\u884c\u6240\u9700\u64cd\u4f5c\u9700\u5177\u5907\u7684\u8f93\u5165\u59ff\u52bf\u94fe\u63a5\u548c\u5c5e\u6027\u3002"),(0,l.kt)("p",null,"\u4e00\u822c\u6765\u8bf4\u9700\u8981\uff1a"),(0,l.kt)("ol",null,(0,l.kt)("li",{parentName:"ol"},"Pose \u8f93\u5165")),(0,l.kt)("p",null,"Pose \u8f93\u5165\u4e00\u822c\u662f\u901a\u8fc7\u521b\u5efa FPoseLink \u6216 FComponentSpacePoseLink \u7c7b\u578b\u7684\u5c5e\u6027\u6765\u516c\u5f00\u4e3a\u4e00\u4e2aPin\u3002\u5176\u4e2d FPoseLink \u7528\u4e8e\u5904\u7406 Local Space \u7684 Pose \u65f6\u4f7f\u7528\uff0c\u4f8b\u5982\u6df7\u5408\u52a8\u753b\u3002FComponentSpacePoseLink \u5728\u5904\u7406 Component Space \u4e2d\u7684 Pose \u65f6\u4f7f\u7528\u3002\u4f8b\u5982\uff1a"),(0,l.kt)("center",null,(0,l.kt)("p",null,(0,l.kt)("img",{alt:"animnode",src:n(6807).Z,width:"993",height:"657"}))),(0,l.kt)("p",null,"\u4e00\u4e2a\u8282\u70b9\u8fd8\u53ef\u4ee5\u6709\u591a\u4e2a Pose \u8f93\u5165\u3002\u53e6\u5916\uff0c\u8fd9\u4e24\u79cd\u7c7b\u578b\u7684\u5c5e\u6027\u53ea\u80fd\u516c\u5f00\u4e3a\u8f93\u5165\u5f15\u811a\uff0c\u65e0\u6cd5\u88ab\u9690\u85cf\u6216\u8005\u4ec5\u4f5c\u4e3a Details \u9762\u677f\u4e2d\u7684\u53ef\u7f16\u8f91\u5c5e\u6027\u3002"),(0,l.kt)("ol",{start:2},(0,l.kt)("li",{parentName:"ol"},"\u5c5e\u6027\u548c\u6570\u636e\u8f93\u5165")),(0,l.kt)("p",null,"\u53ef\u4ee5\u901a\u8fc7 UPROPERTY \u5b8f\u6765\u58f0\u660e\u81ea\u5b9a\u4e49\u5c5e\u6027\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-cpp"},"UPROPERTY(Category=Settings, meta(PinShownByDefault))\nmutable float Alpha;\n")),(0,l.kt)("p",null,"\u4f7f\u7528\u7279\u6b8a\u7684 meta\uff0c\u52a8\u753b\u8282\u70b9\u5c5e\u6027\u53ef\u4ee5\u516c\u5f00\u4e3a\u6570\u636e\u8f93\u5165\u5f15\u811a\uff0c\u4ee5\u5141\u8bb8\u503c\u4f20\u9012\u5230\u8282\u70b9\uff1a"),(0,l.kt)("ul",null,(0,l.kt)("li",{parentName:"ul"},"NeverAsPin\uff1a\u5c06\u5c5e\u6027\u4f5c\u4e3aAnimGraph\u4e2d\u7684\u6570\u636e\u5f15\u811a\u9690\u85cf\uff0c\u5e76\u4e14\u4ec5\u53ef\u5728\u8282\u70b9\u7684 \u7ec6\u8282\uff08Details\uff09 \u9762\u677f\u4e2d\u7f16\u8f91"),(0,l.kt)("li",{parentName:"ul"},"PinHiddenByDefault\uff1a\u5c06\u5c5e\u6027\u4f5c\u4e3a\u5f15\u811a\u9690\u85cf\u3002\u4f46\u662f\u53ef\u4ee5\u901a\u8fc7 Details \u9762\u677f\u8bbe\u7f6e\uff0c\u5c06\u5c5e\u6027\u4f5c\u4e3a\u6570\u636e\u5f15\u811a\u5728AnimGraph\u4e2d\u516c\u5f00"),(0,l.kt)("li",{parentName:"ul"},"PinShownByDefault\uff1a\u5c06\u5c5e\u6027\u4f5c\u4e3a\u6570\u636e\u5f15\u811a\u5728AnimGraph\u4e2d\u516c\u5f00"),(0,l.kt)("li",{parentName:"ul"},"AlwaysAsPin\uff1a\u59cb\u7ec8\u5c06\u5c5e\u6027\u4f5c\u4e3a\u6570\u636e\u70b9\u5728AnimGraph\u4e2d\u516c\u5f00")),(0,l.kt)("h2",{id:"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6"},"\u7f16\u8f91\u5668\u8282\u70b9\u7ec4\u4ef6"),(0,l.kt)("p",null,"\u7f16\u8f91\u5668\u7c7b\u6d3e\u751f\u81ea UAnimGraphNode_Base\uff0c\u8d1f\u8d23\u8282\u70b9\u6807\u9898\u7b49\u89c6\u89c9\u5143\u7d20\u6216\u6dfb\u52a0\u4e0a\u4e0b\u6587\u83dc\u5355\u64cd\u4f5c\u3002\u7f16\u8f91\u5668\u7c7b\u5e94\u8be5\u5305\u542b\u4e00\u4e2a\u516c\u5f00\u4e3a\u53ef\u7f16\u8f91\u7684\u8fd0\u884c\u65f6\u8282\u70b9\u5b9e\u4f8b\uff1a"),(0,l.kt)("pre",null,(0,l.kt)("code",{parentName:"pre",className:"language-cpp"},"UPROPERTY(Category=Settings)\nFAnimNode_ApplyAdditive Node;\n")),(0,l.kt)("h2",{id:"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c"},"\u52a8\u753b\u8282\u70b9\u7684\u8fd0\u4f5c"),(0,l.kt)("p",null,"\u52a8\u753b\u84dd\u56fe\u662f\u4ee5\u8282\u70b9\u6811\u7684\u65b9\u5f0f\u6765\u7ec4\u7ec7\u52a8\u753b\u7684\u8fd0\u884c\u903b\u8f91\u7684\u3002\u5728\u8fd0\u4f5c\u7684\u8fc7\u7a0b\u4e2d\uff0c\u4e3b\u8981\u9700\u8981\u4e86\u89e3\u4e09\u4e2a\u540d\u8bcd\uff1a"),(0,l.kt)("ol",null,(0,l.kt)("li",{parentName:"ol"},"\u66f4\u65b0\uff08Update\uff09")),(0,l.kt)("p",null,"\u8282\u70b9\u7684Update\u7528\u4e8e\u6839\u636eWeight\u8ba1\u7b97\u52a8\u753b\u7684\u5404\u79cd\u6743\u91cd\u3002\u56e0\u4e3aWeight\u4f1a\u5728\u4e0b\u4e00\u9636\u6bb5\u6e05\u7a7a\u3002"),(0,l.kt)("p",null,"\u5982\u679c\u6309\u7167Epic\u7684\u7f16\u5199\u4e60\u60ef\uff0c\u6211\u4eec\u5e94\u8be5\u5728Update\u91cc\u9762\u62ff\u5230\u6240\u6709\u5916\u90e8\u6570\u636e\u5e76\u4e14\u9884\u8ba1\u7b97\uff0c\u4fdd\u8bc1Evaluate\u53ef\u4ee5\u76f4\u63a5\u4f7f\u7528\u3002"),(0,l.kt)("ol",{start:2},(0,l.kt)("li",{parentName:"ol"},"\u8bc4\u4f30\uff08Evaluate\uff09")),(0,l.kt)("p",null,"\u6839\u636e\u4e0a\u4e00\u4e2a\u8282\u70b9\u7684Pose\uff0c\u8ba1\u7b97\u51fa\u8f93\u51fa\u5230\u4e0b\u4e2a\u8282\u70b9\u7684Pose\u3002"),(0,l.kt)("p",null,"\u8fd9\u662f\u52a8\u753b\u8282\u70b9\u6700\u91cd\u8981\u7684\u90e8\u5206\u3002\u6b63\u5e38\u6765\u8bf4\u6211\u4eec\u5e94\u8be5\u628a\u9aa8\u9abc\u8ba1\u7b97\u90e8\u5206\u90fd\u653e\u5728\u8fd9\u91cc\u3002"),(0,l.kt)("p",null,"\u6ce8\u610fUpdate\u548cEvaluate\u90fd\u6709\u53ef\u80fd\u8fd0\u884c\u5728\u5b50\u7ebf\u7a0b\u4e0a\uff0c\u9664\u4e86\u8bfb\u5199AnimInstanceProxy\u5916\uff0c\u64cd\u4f5c\u5176\u4ed6\u4e1c\u897f\u90fd\u4e0d\u662f\u7ebf\u7a0b\u5b89\u5168\u7684\uff0c\u5c3d\u53ef\u80fd\u4e0d\u8981\u78b0\u5916\u90e8\u7684UObject\u3002"),(0,l.kt)("ol",{start:3},(0,l.kt)("li",{parentName:"ol"},"\u6839\u8282\u70b9\uff08Root\uff09")),(0,l.kt)("p",null,"\u4e5f\u53eb OutPut Pose \u8282\u70b9\u3002\u6839\u8282\u70b9\u662f\u6700\u91cd\u8981\u7684\u8282\u70b9\u3002\u5bf9\u4e8e\u7528\u6237\u6765\u8bf4\uff0c\u4ed6\u662f\u6240\u6709\u52a8\u753b\u903b\u8f91\u7684\u8f93\u51fa\u8282\u70b9\u3002\u4f46\u662f\u5bf9\u4e8e\u84dd\u56fe\u6765\u8bf4\uff0c\u4ed6\u662f\u6574\u4e2a\u84dd\u56fe\u8282\u70b9\u7684\u5f00\u59cb\u3002AnimInstance \u5c06\u4ece\u8fd9\u91cc\u5f00\u59cb\u5efa\u7acb\u6574\u4e2a\u52a8\u753b\u8282\u70b9\u7684\u6811\u72b6\u8054\u7cfb\u3002"),(0,l.kt)("p",null,"\u4ee5\u4e0b\u9762\u7684\u84dd\u56fe\u4e3e\u4f8b\uff1a"),(0,l.kt)("center",null,(0,l.kt)("p",null,(0,l.kt)("img",{alt:"work",src:n(6686).Z,width:"1735",height:"1087"}))),(0,l.kt)("ol",null,(0,l.kt)("li",{parentName:"ol"},"\u5728 Update \u7684\u65f6\u5019\uff0c\u6267\u884c Root \u7684 Update"),(0,l.kt)("li",{parentName:"ol"},"Root \u627e\u5230\u8fde\u63a5\u5230\u4ed6\u7684\u4e0a\u4e00\u4e2a\u8282\u70b9\uff0cBlendPosesByBool \u8282\u70b9\uff0c\u6267\u884c\u4ed6\u7684 Update"),(0,l.kt)("li",{parentName:"ol"},"BlendPosesByBool \u8282\u70b9\u7684 Update \u9996\u5148\u4f1a\u8bfb\u53d6\u6240\u6709 Pin \u7684\u503c\uff0c\u8fd9\u4e2a\u8282\u70b9\u6765\u8bf4\u4e3b\u8981\u662f Active Value"),(0,l.kt)("li",{parentName:"ol"},"\u7136\u540e\u6839\u636e Value\uff0c\u6267\u884c\u5bf9\u5e94\u8282\u70b9\u7684 Update\uff0c\u4e5f\u5c31\u662f\u4e0a\u4e00\u4e2a\u8282\u70b9\uff0cBlendSpace Player\u8282\u70b9\u7684 Update"),(0,l.kt)("li",{parentName:"ol"},"BlendSpacePlayer \u7684 Update \u4f1a\u8bfb\u53d6 Speed \u7684\u503c\u5230\u5c5e\u6027\u91cc"),(0,l.kt)("li",{parentName:"ol"},"\u7136\u540e\u662f Evaluate\uff0c\u6cbf\u7740\u540c\u6837\u7684\u7ebf\u8def"),(0,l.kt)("li",{parentName:"ol"},"\u9996\u5148\u6267\u884c Output \u7684 Evaluate\uff0c\u4ed6\u4f1a\u5148\u6267\u884c\u4e0a\u4e00\u4e2a\u8282\u70b9\u7684 Evaluate"),(0,l.kt)("li",{parentName:"ol"},"\u4e5f\u5c31\u662f BlendPosesByBool \u7684 Evaluate\uff0c\u8fd9\u91cc\u4ed6\u4e5f\u4f1a\u5148\u6267\u884c\u5f53\u524d\u6fc0\u6d3b\u7684\u8282\u70b9\u7684 Evaluate\uff0c\u4e5f\u5c31\u662f BlendSpacePlayer \u7684 Evaluate"),(0,l.kt)("li",{parentName:"ol"},"BlendSpacePlayer \u7684 Evaluate \u5f88\u7b80\u5355\uff0c\u6839\u636e\u8f93\u5165\u7684\u53c2\u6570\uff0c\u4ece BlendSpace \u91cc\u9762\u8f93\u51fa\u4e00\u4e2a\u6df7\u5408\u7684 Pose"),(0,l.kt)("li",{parentName:"ol"},"\u7136\u540e\u56de\u5230 BlendPosesByBool\uff0c\u5982\u679c\u6709\u6df7\u5408\u65f6\u95f4\u800c\u4e14\u5904\u4e8e\u6df7\u5408\u72b6\u6001\uff0c\u4ed6\u4f1a\u628a\u4e24\u4e2a Pose \u6309\u7167\u6bd4\u4f8b\u6df7\u5408\u518d\u8f93\u51fa\uff0c\u5982\u679c\u6ca1\u6709\uff0c\u5219\u76f4\u63a5\u539f\u6837\u8f93\u51fa\u5176\u4e2d\u4e00\u4e2a Pose\uff0c\u5f53\u524d\u662f BlendSpace \u7684\u8f93\u51fa"),(0,l.kt)("li",{parentName:"ol"},"\u6700\u540e\u7684Output\u8282\u70b9\u628a\u524d\u9762\u7684Pose\u8f93\u51fa\uff0c\u5927\u529f\u544a\u6210")))}d.isMDXComponent=!0},6807:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/animnode-8b8172652bbedb6e6a33f62bfbc071c8.jpg"},6686:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/animnodework-b79e79cc4c54cf679ce8cddd4e743f25.jpg"}}]); \ No newline at end of file diff --git a/assets/js/runtime~main.fb6e3c90.js b/assets/js/runtime~main.ec730797.js similarity index 99% rename from assets/js/runtime~main.fb6e3c90.js rename to assets/js/runtime~main.ec730797.js index 2d45878..be98fcb 100644 --- a/assets/js/runtime~main.fb6e3c90.js +++ b/assets/js/runtime~main.ec730797.js @@ -1 +1 @@ -(()=>{"use strict";var e,a,f,c,d,b={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var f=t[e]={id:e,loaded:!1,exports:{}};return b[e].call(f.exports,f,f.exports,r),f.loaded=!0,f.exports}r.m=b,r.c=t,e=[],r.O=(a,f,c,d)=>{if(!f){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](f[o])))?f.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[f,c,d]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};a=a||[null,f({}),f([]),f(f)];for(var t=2&c&&e;"object"==typeof t&&!~a.indexOf(t);t=f(t))Object.getOwnPropertyNames(t).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,r.d(d,b),d},r.d=(e,a)=>{for(var f in a)r.o(a,f)&&!r.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,f)=>(r.f[f](e,a),a)),[])),r.u=e=>"assets/js/"+({53:"935f2afb",139:"bde6b81b",496:"8c0e717c",511:"853d7296",514:"0c266c0c",519:"ec51423d",533:"b2b675dd",604:"22155049",832:"87ae2219",873:"737b1ac7",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",4376:"fbf3fddc",4690:"ae4eb2ec",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",6593:"8548c8e1",6751:"d5d750f6",6947:"7ad2b423",6970:"097507e8",7008:"bb21d70f",7206:"5c3b3aa9",7311:"6d47f0aa",7343:"892abff3",7412:"7ea8078a",7414:"393be207",7561:"bd84e224",7918:"17896441",8259:"1a816252",8401:"22db39bf",8610:"6875c492",8691:"24ef8de0",8950:"bc522f7e",9293:"8c4028e7",9348:"e6f10ff9",9514:"1be78505",9581:"314205e2",9671:"0e384e19",9733:"7b8062bd",9817:"14eb3368",9978:"97db2e09"}[e]||e)+"."+{53:"063ffdef",139:"ea1302cc",496:"ec606d54",511:"15b16dca",514:"7e6b2be1",519:"dfb088d6",533:"1d592338",604:"23cdd8d7",832:"3d043bba",873:"71c3a753",893:"2a63fb9c",1138:"3651cb71",1266:"4f84ef89",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:"03b6ab49",3805:"80c166eb",3910:"b671f4bf",3946:"fe29b4d4",3988:"18eb2e9a",4013:"b24b06c3",4195:"7d326ab3",4322:"9c1d7a11",4376:"fea9740c",4690:"90492568",4716:"3f6d0941",4776:"7b2c0f96",4900:"079317d1",4972:"ffa93c54",5090:"17426698",5150:"8e7e6577",5210:"903f3267",5432:"4d933abf",5788:"a5662690",6103:"9afd9042",6171:"9c354001",6208:"843037eb",6219:"8b3efbc2",6474:"3374a8a0",6593:"f056ff23",6751:"d38fa352",6947:"0ebba826",6970:"0ce59990",7008:"b2b55af3",7206:"5a928f9b",7311:"cc155c8d",7343:"a88d1807",7412:"4a0719df",7414:"0a2105a2",7561:"90d69d8e",7918:"5722986e",8259:"cbe87a39",8401:"3c3a7c30",8610:"c824e621",8691:"2e6b65b3",8950:"834efd11",9293:"4e4268a6",9348:"ff60fb03",9514:"aa7a9923",9581:"6c0d6443",9671:"eee46400",9733:"3112f7ac",9817:"e4b6cfb0",9978:"b5a1db5e"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),c={},d="ryao-blog:",r.l=(e,a,f,b)=>{if(c[e])c[e].push(a);else{var t,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=c[e];if(delete c[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(f))),a)return a(f)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=u.bind(null,t.onerror),t.onload=u.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",22155049:"604","935f2afb":"53",bde6b81b:"139","8c0e717c":"496","853d7296":"511","0c266c0c":"514",ec51423d:"519",b2b675dd:"533","87ae2219":"832","737b1ac7":"873","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",fbf3fddc:"4376",ae4eb2ec:"4690","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","8548c8e1":"6593",d5d750f6:"6751","7ad2b423":"6947","097507e8":"6970",bb21d70f:"7008","5c3b3aa9":"7206","6d47f0aa":"7311","892abff3":"7343","7ea8078a":"7412","393be207":"7414",bd84e224:"7561","1a816252":"8259","22db39bf":"8401","6875c492":"8610","24ef8de0":"8691",bc522f7e:"8950","8c4028e7":"9293",e6f10ff9:"9348","1be78505":"9514","314205e2":"9581","0e384e19":"9671","7b8062bd":"9733","14eb3368":"9817","97db2e09":"9978"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(a,f)=>{var c=r.o(e,a)?e[a]:void 0;if(0!==c)if(c)f.push(c[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((f,d)=>c=e[a]=[f,d]));f.push(c[2]=d);var b=r.p+r.u(a),t=new Error;r.l(b,(f=>{if(r.o(e,a)&&(0!==(c=e[a])&&(e[a]=void 0),c)){var d=f&&("load"===f.type?"missing":f.type),b=f&&f.target&&f.target.src;t.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,c[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,f)=>{var c,d,b=f[0],t=f[1],o=f[2],n=0;if(b.some((a=>0!==e[a]))){for(c in t)r.o(t,c)&&(r.m[c]=t[c]);if(o)var i=o(r)}for(a&&a(f);n{"use strict";var e,a,f,c,d,b={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var f=t[e]={id:e,loaded:!1,exports:{}};return b[e].call(f.exports,f,f.exports,r),f.loaded=!0,f.exports}r.m=b,r.c=t,e=[],r.O=(a,f,c,d)=>{if(!f){var b=1/0;for(i=0;i=d)&&Object.keys(r.O).every((e=>r.O[e](f[o])))?f.splice(o--,1):(t=!1,d0&&e[i-1][2]>d;i--)e[i]=e[i-1];e[i]=[f,c,d]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},f=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,c){if(1&c&&(e=this(e)),8&c)return e;if("object"==typeof e&&e){if(4&c&&e.__esModule)return e;if(16&c&&"function"==typeof e.then)return e}var d=Object.create(null);r.r(d);var b={};a=a||[null,f({}),f([]),f(f)];for(var t=2&c&&e;"object"==typeof t&&!~a.indexOf(t);t=f(t))Object.getOwnPropertyNames(t).forEach((a=>b[a]=()=>e[a]));return b.default=()=>e,r.d(d,b),d},r.d=(e,a)=>{for(var f in a)r.o(a,f)&&!r.o(e,f)&&Object.defineProperty(e,f,{enumerable:!0,get:a[f]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,f)=>(r.f[f](e,a),a)),[])),r.u=e=>"assets/js/"+({53:"935f2afb",139:"bde6b81b",496:"8c0e717c",511:"853d7296",514:"0c266c0c",519:"ec51423d",533:"b2b675dd",604:"22155049",832:"87ae2219",873:"737b1ac7",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",4376:"fbf3fddc",4690:"ae4eb2ec",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",6593:"8548c8e1",6751:"d5d750f6",6947:"7ad2b423",6970:"097507e8",7008:"bb21d70f",7206:"5c3b3aa9",7311:"6d47f0aa",7343:"892abff3",7412:"7ea8078a",7414:"393be207",7561:"bd84e224",7918:"17896441",8259:"1a816252",8401:"22db39bf",8610:"6875c492",8691:"24ef8de0",8950:"bc522f7e",9293:"8c4028e7",9348:"e6f10ff9",9514:"1be78505",9581:"314205e2",9671:"0e384e19",9733:"7b8062bd",9817:"14eb3368",9978:"97db2e09"}[e]||e)+"."+{53:"063ffdef",139:"ea1302cc",496:"ec606d54",511:"15b16dca",514:"7e6b2be1",519:"dfb088d6",533:"1d592338",604:"23cdd8d7",832:"3d043bba",873:"71c3a753",893:"2a63fb9c",1138:"3651cb71",1266:"4f84ef89",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:"03b6ab49",3805:"80c166eb",3910:"b671f4bf",3946:"fe29b4d4",3988:"18eb2e9a",4013:"b24b06c3",4195:"7d326ab3",4322:"9c1d7a11",4376:"fea9740c",4690:"90492568",4716:"3f6d0941",4776:"7b2c0f96",4900:"079317d1",4972:"ffa93c54",5090:"17426698",5150:"8e7e6577",5210:"903f3267",5432:"4d933abf",5788:"a5662690",6103:"9afd9042",6171:"9c354001",6208:"843037eb",6219:"8b3efbc2",6474:"3374a8a0",6593:"f056ff23",6751:"d38fa352",6947:"0ebba826",6970:"0ce59990",7008:"b2b55af3",7206:"5a928f9b",7311:"cc155c8d",7343:"a88d1807",7412:"4a0719df",7414:"0a2105a2",7561:"90d69d8e",7918:"5722986e",8259:"cbe87a39",8401:"3c3a7c30",8610:"c824e621",8691:"2e6b65b3",8950:"834efd11",9293:"25fb4b27",9348:"ff60fb03",9514:"aa7a9923",9581:"6c0d6443",9671:"eee46400",9733:"3112f7ac",9817:"e4b6cfb0",9978:"b5a1db5e"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),c={},d="ryao-blog:",r.l=(e,a,f,b)=>{if(c[e])c[e].push(a);else{var t,o;if(void 0!==f)for(var n=document.getElementsByTagName("script"),i=0;i{t.onerror=t.onload=null,clearTimeout(s);var d=c[e];if(delete c[e],t.parentNode&&t.parentNode.removeChild(t),d&&d.forEach((e=>e(f))),a)return a(f)},s=setTimeout(u.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=u.bind(null,t.onerror),t.onload=u.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",22155049:"604","935f2afb":"53",bde6b81b:"139","8c0e717c":"496","853d7296":"511","0c266c0c":"514",ec51423d:"519",b2b675dd:"533","87ae2219":"832","737b1ac7":"873","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",fbf3fddc:"4376",ae4eb2ec:"4690","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","8548c8e1":"6593",d5d750f6:"6751","7ad2b423":"6947","097507e8":"6970",bb21d70f:"7008","5c3b3aa9":"7206","6d47f0aa":"7311","892abff3":"7343","7ea8078a":"7412","393be207":"7414",bd84e224:"7561","1a816252":"8259","22db39bf":"8401","6875c492":"8610","24ef8de0":"8691",bc522f7e:"8950","8c4028e7":"9293",e6f10ff9:"9348","1be78505":"9514","314205e2":"9581","0e384e19":"9671","7b8062bd":"9733","14eb3368":"9817","97db2e09":"9978"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(a,f)=>{var c=r.o(e,a)?e[a]:void 0;if(0!==c)if(c)f.push(c[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var d=new Promise(((f,d)=>c=e[a]=[f,d]));f.push(c[2]=d);var b=r.p+r.u(a),t=new Error;r.l(b,(f=>{if(r.o(e,a)&&(0!==(c=e[a])&&(e[a]=void 0),c)){var d=f&&("load"===f.type?"missing":f.type),b=f&&f.target&&f.target.src;t.message="Loading chunk "+a+" failed.\n("+d+": "+b+")",t.name="ChunkLoadError",t.type=d,t.request=b,c[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,f)=>{var c,d,b=f[0],t=f[1],o=f[2],n=0;if(b.some((a=>0!==e[a]))){for(c in t)r.o(t,c)&&(r.m[c]=t[c]);if(o)var i=o(r)}for(a&&a(f);n - +
- + \ No newline at end of file diff --git a/blog/archive.html b/blog/archive.html index 2712dda..2f4ad36 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 37532af..59f677d 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 3e6081c..dd16612 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 ba1ca8d..524c43e 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 d399b66..59bac32 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 610c03f..de6d811 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 732f839..e79aa2a 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 7fe7f6b..e72166c 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 8edb82b..54d9b79 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 46572d4..1d9d631 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 @@ - +

C++系列

Ryao的C++知识库,主要记录一些不那么容易记住的小知识。

- + \ 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 636e61a..f3734bc 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 7e8dbd1..afd6021 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 c19240f..7440a0e 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 e3c76bb..3ad7513 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 6352e22..2d72a22 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 4bf2819..7d35c4a 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 598ce0d..4d8cb79 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 fc8c526..54d8b0c 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 76893aa..ba9f755 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 3fb9da9..5b79a4c 100644 --- a/docs/collision-series/introduction.html +++ b/docs/collision-series/introduction.html @@ -9,13 +9,13 @@ - +

碰撞处理简介

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

内点法(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 e1fdf1a..f429ddb 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 f62ddc7..f5e4877 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 d05514b..b50a2ef 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 7d3b1dc..f233bb9 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/auto_decltype.html b/docs/cpp-series/auto_decltype.html index 02727c7..77b1bbb 100644 --- a/docs/cpp-series/auto_decltype.html +++ b/docs/cpp-series/auto_decltype.html @@ -9,14 +9,14 @@ - +

自动类型推导

C++ 11 中引用了 auto 和 decltype 两个关键字实现了类型推导,让编译器来操心变量的类型。其中,auto 只能用来声明变量,而 decltype 则更加通用。

auto 类型推导

auto 声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。从这个意义上来讲,auto 并非一种“类型”说明,而是一个类型声明时的“占位符”,编译器在编译时期会将 auto 替代为变量实际的类型。

auto 推导的最大优势是在拥有初始化表达式的复杂类型变量时简化代码,比如 STL 库的各种容器的迭代器。 例如 vector<int>begin()返回的是vector<int>::const_iteratior类型,这个名字太长了,就可以直接用一个 auto 来代替。

推导规则

  • 对于指针类型,声明为 auto、auto* 是没有区别的
  • 声明引用类型,必须使用 auto &
  • 不能得到推导变量的 const、volatile 属性
  • auto不能用于函数传参
  • auto还不能用于推导数组类型

其中,关于不能得到推导变量的 const、volatile 属性体现在:如果有一个函数返回的类型为 const int,如果直接用一个 auto 来接受的话,auto 会被推导为一个普通的 int。所以如果希望继承相关属性的话,需要自己显式声明,例如:

const auto value = returnAConstInt();

如果使用花括号 {} 来初始化变量,则不能使用 auto 来推导类型,否则会被推导为 std::initializer_list<int>,例如:

auto x = {1}; // x 是 std::initializer_list<int>{1}
auto y = (1); // y 是 int

auto 只是 C++ 11 类型推导的一部分,还有一部分应该使用 decltype 来体现。

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。它的用法和 sizeof 很相似:

decltype(表达式)

可以看到,他最主要的特点在于可以计算某个表达式的类型,例如:

auto x = 1;
auto y = 2;
decltype(x+y) z;

推导规则

规则推导依次进行,满足任一条件就退出推导:

  • 单个标记符,推导为T,例如 int a; decltype(a) b;,推导为int
  • 右值引用,推导为T&&,例如int a, decltype(move(a)) b; 推导为int&&
  • 非单个标记符的左值,推导为T&,例如
double arr[5]
decltype(++a) aaa = a; // 两个标记 ++ 和 a,推导为 int &
decltype(arr[3]) arrv = 1.0; // 非单个标记,推导为 double &
decltype("Hello") h = "hello"; // 字符串字面值为左值,推导为 const char (&)[6]
  • 不满足上面三个条件就推导为T

所以,对于 int i,编译器 decltype(i) 和 decltype((i)) 的编译结果不同,因为 i 是单个标记符,推导为 int;(i) 是标记符加小括号,不满足规则一,但满足规则三,推导为 int &。如果 decltype((i)) 没有给予左值 int 就会报错——左值引用未初始化。

- + \ No newline at end of file diff --git a/docs/cpp-series/exception.html b/docs/cpp-series/exception.html index f76ba53..fb1f8a6 100644 --- a/docs/cpp-series/exception.html +++ b/docs/cpp-series/exception.html @@ -9,13 +9,13 @@ - +

异常

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

异常处理这一块主要涉及到了三个关键字:trycatchthrow

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch - 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。

其中 try 和 catch 必须一起用,try 块中放入可能抛出异常的代码,这一块的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

catch 能够捕获到 try 块中使用 throw 抛出的异常。如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。

异常一般使用 <exception> 中的 std::exception。但是也可以是自己定义的数据结构,然后在 catch 块中进行一些自定义操作,例如:

#include <iostream>
using namespace std;

double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}

int main ()
{
int x = 50;
int y = 0;
double z = 0;

try {
z = division(x, y);
cout << z << endl;
}catch (const char* msg) {
cerr << msg << endl;
}

return 0;
}

C++ 标准的异常

C++ 提供了一系列标准的异常,定义在 <exception> 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:

exception

异常描述
std::exception该异常是所有标准 C++ 异常的父类。
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_typeid该异常可以通过 typeid 抛出。
std::bad_exception这在处理 C++ 程序中无法预期的异常时非常有用。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector。
std::runtime_error理论上不可以通过读取代码来检测到的异常。
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error当发生数学下溢时,会抛出该异常。

定义新的异常

通过继承 std::exception 来实现自己的异常类,只需要重写 what 函数即可

#include <iostream>
#include <exception>
using namespace std;

struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
}
};

int main()
{
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//其他的错误
}
}
- + \ No newline at end of file diff --git a/docs/cpp-series/functions.html b/docs/cpp-series/functions.html index 76f064e..3d6316c 100644 --- a/docs/cpp-series/functions.html +++ b/docs/cpp-series/functions.html @@ -9,13 +9,13 @@ - +

C++内置函数的机制记录

sizeof

作用非常简单:求对象或者类型的大小(字节数)。其特性如下:

  1. sizeof是运算符,不是函数;
  2. sizeof不能求得void类型的长度;
sizeof(void); //非法
  1. sizeof能求得void类型的指针的长度;
  2. 当表达式作为sizeof的操作数时,它返回表达式的计算结果的类型大小,但是它不对表达式求值
  3. sizeof可以对函数调用求大小,并且求得的大小等于返回类型的大小,但是不执行函数体
int function()
{
cout << "call this function" << endl;
return 1;
}
cout << sizeof(function()) << endl;
// 输出4,而不会输出"call this function"
  1. 结构体或者类中的static成员变量不会被sizeof计算在内

vector 的 resize 和 reserve

首先,reserve 的作用是更改 vector 的容量(capacity),使 vector 至少可以容纳 n 个元素。

  • 如果 reverse(n) 的 n 大于当前 vector 容量,就会进行扩容;其他情况下都不会重新分配 vector 的存储空间
  • reserve 是容器预留空间,但是在空间不创建元素对象,所以在没有添加新的对象之前,不能引用容器内的元素。加入新的元素需要用 push_back 等方法
  • reserve 预分配的空间没有被初始化,所以不可访问

作用:与直接从 0 开始 push 元素,reserve 预分配空间后的 vector 不会频繁四栋分配空间,所以在要 push 的元素很多的情况下可以带来效率提升。

resize 是改变容器的大小,并且创建对象

  • resize(n) 会调整容器的大小(size),使其能够容纳 n 个元素,如果 n 小于容器当前的 size,则会删除多出来的元素
  • resize(n, t) 将所有添加的元素初始化为 t
  • resize 是否会影响 vector 的容量(capacity),要看调整之后容器的 size 是否大于 capacity
  • 由于创建了对象,所以 resize 后可以直接使用 operator[] 来引用元素对象
- + \ No newline at end of file diff --git a/docs/cpp-series/lambda.html b/docs/cpp-series/lambda.html index 26e2989..2e089de 100644 --- a/docs/cpp-series/lambda.html +++ b/docs/cpp-series/lambda.html @@ -9,14 +9,14 @@ - +

Lambda 表达式

C++11 标准中引入了 Lambda 表达式,用于定义匿名函数,使得代码更加灵活简洁。

Lambda 表达式与普通函数类似,也有参数列表、返回值类型和函数体,只是它的定义方式更简洁,并且可以在函数内部定义。例如:

auto plus = [] (int v1, int v2) -> int { return v1 + v2; }
int sum = plus(1, 2);

但是一般来说我们不会这么使用,更多的时候,都是和 STL 的一些算法结合使用,将 lambda 表达式作为一种函数对象传进去。例如:

sort(vec.begin(), vec.end(), [](const int& a, const int& b) 
{
return a > b;
});

这样的写法可以让代码更加简洁、清晰,可读性更强。 一般来说,lambda 的写法是:

[captures](params){body}
  • captures 捕获列表:lambda 可以把上下文变量以值或者引用的方式捕获,在 body 中直接使用
  • params 参数列表:在C++ 14之后才允许使用auto 作为参数类型
  • body 函数体:函数的具体逻辑

捕获列表

几种常见的捕获方式有:

  • [] 什么也不捕获,无法在 lambda 函数体中使用任何上下文中的变量
  • [=] 按值传递的方式捕获所有上下文中的变量
  • [&] 按照引用传递的方式捕获所有上下文中的变量
  • [=, &a] 除了变量 a 是引用传递以外,使用值传递的方式捕获其他的所有变量,后面可以跟多个 &b, &c....
  • [&, a] 以值传递的方式捕获变量 a,使用引用传递的方式部或其他的所有变量
  • [a, &b] 以值传递的方式捕获 a,以引用传递的方式捕获 b
  • [this] 在成员函数中,也可以直接捕获 this 指针(其实在成员函数中,[=][&]也可以直接捕获 this 指针。捕获 this 指针,可以直接调用类中的成员,而不需要用 this->这样的操作

编译器如何处理 lambda 表达式

编译器会将 lambda 表达式翻译成一个类,并且通过重载 operator() 来实现函数调用。

auto lambda = [&]() { cout << "hello!" << endl; return 0; };

cout << typeid(lambda).name() << endl;

// 输出为:
// class <lambda_91e1f7da3da1dbc8aa6d23953317ea83>

例如对如下这个简单的 lambda 表达式:

auto plus = [] (int a, int b) -> int { return a + b; }
int c = plus(1, 2);

编译器会将其翻译为:

class LambdaClass
{
public:
int operator () (int a, int b) const
{
return a + b;
}
};

LambdaClass plus;
int c = plus(1, 2);

而如果该 lambda 表达式中存在变量捕获,那么就会将捕获的变量作为类成员变量存起来,从而在函数体调用的时候可以正常使用。例如(注意按值捕获和按引用捕获是有区别的):

  1. 按值捕获
int x = 1; int y = 2;
auto plus = [=] (int a, int b) -> int { return x + y + a + b; };
// auto plus = [=] (int a, int b) mutable -> int { x++; return x + y + a + b; };
int c = plus(1, 2);

// 翻译结果
class LambdaClass
{
public:
LambdaClass(int xx, int yy)
: x(xx), y(yy) {}

// 运算符重载函数是const的,如果在函数内部要修改捕获的值,需要使用mutable关键字
int operator () (int a, int b) const
{
return x + y + a + b;
}

private:
int x;
int y;
}

int x = 1; int y = 2;
LambdaClass plus(x, y);
int c = plus(1, 2);
  1. 按引用捕获
int x = 1; int y = 2;
auto plus = [&] (int a, int b) -> int { x++; return x + y + a + b;};
int c = plus(1, 2);

// 翻译结果
class LambdaClass
{
public:
LambdaClass(int& xx, int& yy)
: x(xx), y(yy) {}

int operator () (int a, int b)
{
x++;
return x + y + a + b;
}

private:
int &x;
int &y;
};

使用 lambda 表达式的注意事项

  1. 注意捕获的参数的生命周期,例如当前使用[&]捕获的参数在另一个线程已经被释放掉了,但是 lambda 的函数体中仍然对其进行了读写
  2. 引用捕获会导致闭包包含一个局部变量的引用或者一个形参的引用(在定义lamda的作用域)。如果一个由lambda创建的闭包的生命期超过了局部变量或者形参的生命期,那么闭包的引用将会空悬
  3. 谨慎使用或者不用外部指针。如果你用值捕获了个指针,你在lambda创建的闭包中持有这个指针的拷贝,但你不能阻止lambda外面的代码删除指针指向的内容,从而导致你拷贝的指针空悬。
  4. 避免使用默认捕获模式(即[=][&],它可能导致你看不出悬空引用问题)
- + \ No newline at end of file diff --git a/docs/cpp-series/memory.html b/docs/cpp-series/memory.html index e539fd1..55fad3a 100644 --- a/docs/cpp-series/memory.html +++ b/docs/cpp-series/memory.html @@ -9,14 +9,14 @@ - +

类和结构体的内存排布

首先给出基本的内置类型在内存中存储时占用的内存。

Typex86x64
指针48
char11
int44
short22
long44
long long88
float44
double88
long double88

类和结构体的内存对齐结构是按照成员中的最大字节数来存储的。若类或结构体中最大的基本内置数据类型占8个字节,那么类或结构体存储的内存中按照8个字节位一格进行存储,将其他较少字节数的数据往里填,若有空余,则看下一个数据能否填入;若不能,则按照内存对齐原则,从下一格开始填入数据,空余的内存则被跳过。

例如:(其中的struct可以替换成class

struct stus{
char *p; //4
char arr[2]; //1*2
int c; //4
short d; //2
double f; //8
long g; //4
float h[2]; //4*2
};

可以看到这里最大的数据类型是8。注意,是 double 的8,而不是 float[2] 的8,因为在内存中 float h[2];其实和float h0; float h1;是一样的。 那么这个类/结构体的内存排布为:

memory

其中红色圈是空余的内存,整个类/结构体占内存位 5*8=40 字节。

如果结构体和类之间出现了嵌套,那么编译器会将嵌套结构完全拆散成内置数据类型构成的一个类/结构体,然后进行内存排布,例如:

struct A {
int a;
double b;
int c;
};

struct B {
int d;
A e;
int f;
}

可以看到 B 结构体中嵌套了一个结构体 A。那么在编译器看来,在内存排布的时候结构体 B 其实是这样的:

struct AandB {
int d
int a;
double b;
int c;
int f;
}

所以最长的内置数据类型长度为8。那么 B 的长度就是 3×8=243\times8=24 吗?不是的,经过测试,拆开之后的结构只能用于判断对齐长度。但是在上面的代码中,d和a、f和c分别是属于 B 和 A 的成员,它们之间是不允许挤在一起放在一个格子里的,即无法让d和a(c和f)构成一个八字节,最终实现3×8=243\times8=24 的结构。而应该是 5×8=405 \times 8 = 40

为什么要内存对齐?

因为大多数处理器是2个字节、4个字节、甚至更多的字节为单位来存取内存。如果没有内存对齐机制,假如有一个 int 类型的变量放在地址为1的连续4个字节地址中。当处理器获取数据时,它会先从0地址开始读4个字节,然后剔除不想要的字节,再从4地址开始,读取4个字节,再提出不想要的字节,最后再将剩余数据合并。

所以说,内存对齐后可以增加我们访问数据时候的效率。

- + \ No newline at end of file diff --git a/docs/cpp-series/new_feature.html b/docs/cpp-series/new_feature.html index 0b95b9e..bf5f26f 100644 --- a/docs/cpp-series/new_feature.html +++ b/docs/cpp-series/new_feature.html @@ -9,13 +9,13 @@ - +

C++ 新特性

C++ 11

新特性包括:

  • nullptr 替代 NULL引入了
  • auto 和 decltype 这两个关键字实现了类型推导
  • 基于范围的 for 循环for(auto& i : res){}
  • 类和结构体的中初始化列表
  • Lambda 表达式(匿名函数)
  • std::forward_list(单向链表)
  • 右值引用和move语义

C++ NULL 和 nullptr

NULL来自C语言,一般由宏定义实现,而 nullptr 则是C++11的新增关键字。在C语言中,NULL被定义为(void*)0,而在C++语言中,NULL则被定义为整数0。nullptr在C++11被引入用于解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。

智能指针

具体见文章:智能指针

Lambda 表达式

具体见文章:Lambda

右值引用和 move 语义

具体见文章:右值

自动类型推导

具体见文章:自动类型推导

- + \ No newline at end of file diff --git a/docs/cpp-series/rvalue_reference.html b/docs/cpp-series/rvalue_reference.html index 79e9147..1610062 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/smart_ptr.html b/docs/cpp-series/smart_ptr.html index 595a9e3..ec2dddf 100644 --- a/docs/cpp-series/smart_ptr.html +++ b/docs/cpp-series/smart_ptr.html @@ -9,13 +9,13 @@ - +

智能指针

智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏。智能指针一共有四种,其中 auto_ptr 为 C++ 98 提出,其余的三者是在C++ 11 提出的。

  • unique_ptr
  • shared_ptr
  • weak_ptr
  • auto_ptr

auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存。

C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!C++11后不建议使用auto_ptr。为什么呢?主要是一下几个原因:

  1. 复制或者赋值都会改变资源的所有权

根据源码,auto_ptr 的拷贝是这样的:

auto_ptr(auto_ptr& _Right) noexcept : _Myptr(_Right.release()) {}

_Ty* release() noexcept {
_Ty* _Tmp = _Myptr;
_Myptr = nullptr;
return _Tmp;
}

假设有两个 auto_ptr p1 和 p2, 如果使用p1 = p2 进行赋值之后,p2 所管理的资源就会被释放掉。

  1. 不支持数组的内存管理
// 这样写是非法的
auto_ptr<int[]> array(new int[5]);

所以,C++11用更严谨的unique_ptr 取代了auto_ptr!

unique_ptr

unique_ptr 的功能和 auto_ptr 大致相同,但是做了一些设计让它更加安全和符合直觉:

  1. 无法进行进行左值拷贝操作,允许临时右值拷贝构造和拷贝
unique_ptr<int> p1(new int(1));
unique_ptr<int> p2(new int(2));

p1 = p2; // 这样是非法的,禁止拷贝
unique<int> p3(p2); // 这样是非法的,禁止拷贝构造

// 在程序员清楚风险的情况下,通过移动语义去调用移动构造和移动赋值
unique<int> p3(move(p2));
p1 = move(p2);
  1. 支持数组的内存管理

下面这种用法是合法的了

// 会自动调用delete [] 函数去释放内存
unique_ptr<int[]> array(new int[5]);

除了离开作用域自动释放以外,还可以主动进行释放,有以下几种方式:

unique_ptr<int> p(new int(1));
p = NULL;
p = nullptr;
p.reset();

还可以放弃对象的控制权(资源不释放):

int* np = p.release();

auto_ptr 和 unique_ptr 都具有排他性,即一个资源只能被一个 auto_ptr/unique_ptr 管理,如果需要多个指针管理一个资源,就需要 shared_ptr。

shared_ptr

shared_ptr 为了支持多个指针管理同一份资源,维护了一份记录引用特定内存对象的智能指针数量的引用计数,当复制或者拷贝的时候,引用计数加1,当智能指针析构的时候,引用计数减1,如果计数为0,那就释放该资源。

我们可以使用一个普通指针或者另一个 shared_ptr 来初始化一个 shared_ptr。

shared_ptr<int> up1(new int(10));  // int(10) 的引用计数为1
shared_ptr<int> up2(up1); // 使用智能指针up1构造up2, 此时int(10) 引用计数为2

另外还可以使用 make_shared 来初始化,这样的分配内存效率更高,推荐使用。make_shared 函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。

shared_ptr<int> up3 = make_shared<int>(2); // 多个参数以逗号','隔开,最多接受十个
shared_ptr<string> up4 = make_shared<string>("字符串");
shared_ptr<Person> up5 = make_shared<Person>(9);

shared_ptr 的移动构造:

shared_ptr<int> p1 = make_shared<int>(1);
shared_ptr<int> p2(move(p1));

cout << p2.use_count() << endl; // 1
cout << p1.use_count() << endl; // 0
cout << p1.get() << endl; // nullptr

使用 shared_ptr 的过程中可能出现循环引用的问题:

class B;
class A {
public:
shared_ptr<B> ptr;
};

class B {
public:
shared_ptr<A> ptr;
};

int main()
{
shared_ptr<A> a = make_shared<A>(); // 引用计数为1
shared_ptr<B> b = make_shared<B>(); // 引用计数为1
a->ptr = b; // 引用计数为2
b->ptr = a; // 引用计数为2
cout << a->ptr.use_count() << endl; // 2
cout << b->ptr.use_count() << endl; // 2
}

在析构过程中,对程序中实际生成的 A 和 B 的实例,都有 a、b->ptr 和 b、a->ptr 指向它们,程序运行结束的时候,a 和 b 被释放掉,可是实例中仍然有一个 shared_ptr 指向彼此,所以资源实际上并不会被正确释放掉。这个问题可以使用 weak_ptr 来解决。

可以发现,上面的代码中我们删掉 a->ptr = b 或者 b->ptr = a 中的任意一行都可以解决这个问题,weak_ptr 就是这样的工作原理。

weak_ptr

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载 *-> 但可以使用 lock 获得一个可用的 shared_ptr 对象,从而来获得数据。

weak_ptr wpGirl_1; // 定义空的弱指针

weak_ptr wpGirl_2(spGirl); // 使用共享指针构造

wpGirl_1 = spGirl; // 允许共享指针赋值给弱指针

weak_ptr 也可以获得引用计数:

wpGirl_1.use_count();

在需要访问数据的时候可以通过 lock() 方法获得 shared_ptr,使用后记得将获得的 shared_ptr 释放掉就好:

shared_ptr<Girl> sp_girl;
sp_girl = wpGirl_1.lock();

// 使用完之后,再将共享指针置NULL即可
sp_girl = NULL;

为什么智能指针可以像普通指针那样使用?

因为智能指针里面重载了 *-> 运算符。

智能指针使用tips

  1. 不要把一个原生指针给多个智能指针管理,因为资源可能会被某个智能指针擅自释放掉了。(这个和多个 shared_ptr 管理一个资源不一样,如果需要多个智能指针来管理一个资源,请用 shared_ptr 的拷贝或者构造)
  2. 禁止 delete 智能指针 get 函数返回的指针,如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
  3. 禁止用任何类型智能指针 get 函数返回的指针去初始化另外一个智能指针

自己写一个 shared_ptr !

要点主要是,使用指针来存储引用计数,从而就能够让sharedPtr共享同一个引用计数。

class SharedPtr {
public:
SharedPtr(int* p) : ptr(p), refCount(new int(1)) {}

SharedPtr(const SharedPtr& other) : ptr(other.ptr), refCount(other.refCount) {
(*refCount)++;
}

SharedPtr(SharedPtr&& other) {
ptr = other.ptr;
other.ptr = nullptr;
refCount = other.refCount;
other.refCount = nullptr;
}

~SharedPtr() {
(*refCount)--;
if (*refCount == 0) {
delete ptr;
delete refCount;
}
}

SharedPtr& operator=(const SharedPtr& other) {
if (this == &other) {
return *this;
}

(*refCount)--;
if (*refCount == 0) {
delete ptr;
delete refCount;
}

ptr = other.ptr;
refCount = other.refCount;
(*refCount)++;

return *this;
}

int* get() const {
return ptr;
}

int& operator*() const {
return *ptr;
}

int* operator->() const {
return ptr;
}

int* ptr;
int* refCount;
};
- + \ No newline at end of file diff --git a/docs/cpp-series/template.html b/docs/cpp-series/template.html index 9e871c6..a6a34a1 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/thread.html b/docs/cpp-series/thread.html index 82927bd..5172927 100644 --- a/docs/cpp-series/thread.html +++ b/docs/cpp-series/thread.html @@ -9,7 +9,7 @@ - + @@ -22,7 +22,7 @@ 那么就可以用 std::lock_guard,它会在构造函数里调用 mtx.lock(),析构函数里会调用 mtx.unlock(),从而退出当前作用域时会自动解锁。

vector<int> arr;
mutex mtx;
thread t1([&] {
for (int i = 0; i < 1000; i++) {
lock_guard grd(mtx);
arr.push_back(1);
}
});

thread t2([&] {
for (int i = 0; i < 1000; i++) {
lock_guard grd(mtx);
arr.push_back(2);
}
});
t1.join();
t2.join();

std::unique_lock

std::lock_guard 会严格在析构的时候调用 unlock(),这样并不灵活,如果我们想要提前进行解锁的话,就可以使用 std::unique_lock。它额外存储了一个 flag 表示是否已经被释放。他会在析构检测这个 flag,如果没有释放,则调用 unlock(),否则不调用。

同样,在有需要的时候 std::unique_lock 还可以再次上锁,unique_lock 和 mutex 具有同样的接口:

vector<int> arr;
mutex mtx;
thread t1([&] {
for (int i = 0; i < 1000; i++) {
unique_lock grd(mtx);
arr.push_back(1);
}
});

thread t2([&] {
for (int i = 0; i < 1000; i++) {
unique_lock grd(mtx);
arr.push_back(2);
grd.unlock();
// grd.lock();
}
});
t1.join();
t2.join();

死锁

由于两个线程的指令执行并不是同步的,所以可能出现如下情况:

mutex mtx1;
mutex mtx2;

thread t1([&] {
for (int i = 0; i < 1000; i++) {
mtx1.lock();
mtx2.lock();
mtx1.unlock();
mtx2.unlock();
}
});

thread t2([&] {
for (int i = 0; i < 1000; i++) {
mtx2.lock();
mtx1.lock();
mtx1.unlock();
mtx2.unlock();
}
});

t1.join();
t2.join();

可能出现这个情况:

  • t1 给 mtx1 上锁
  • t2 给 mtx2 上锁
  • t1 尝试给 mtx2 上锁,但是已经上锁了,于是开始等待
  • t2 尝试给 mtx1 上锁,但是已经上锁了,于是开始等待
  • 双方都在等待释放锁,开始无限制等待

这种现象就称为死锁。解决方案有:

  • 最简单的是一个线程不要持有多个锁,但是有时候这是无法实现的
  • 多个线程保证一样的上锁顺序
  • 使用 std::lock(mtx1, mtx2, ...)一次性对多个 mutex 上锁,这个函数可以接受任意多个 mutex 作为参数,并且保证一定不会产生死锁问题,std::lock 需要手动解锁,使用 std::scoped_lock 可以实现自动解锁

另一个死锁例子

同一个线程重复调用 lock 也可以造成死锁,例如:

mutex mtx1;

void other() {
mtx1.lock();
// do something
mtx1.unlock();
}

void func() {
mtx1.lock();
other();
mtx1.unlock();
}

实现线程安全的 vector

可以通过 mutex 来给 vector 的写入和读取进行上锁,从而实现一个线程安全的 vector:

  • 写入同时只能有一个人操作,所以要严格上锁
  • 读取过程可以多人同时操作,所以不需要严格上锁

std::shared_mutex 可以实现上述两个功能:

class MTVector {
private:
std::vector<int> vec;
std::shared_mutex mtx;

public:
size_t size() {
mtx.lock_shared();
size_t ret = vec.size();
mtx.unlock_shared();
return ret;
}

void push_back(int val) {
mtx.lock();
vec.push_back(val);
mtx.unlock();
}
};

原子操作

原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为"不可被中断的一个或一系列操作" 。

有两个方式来实现原子操作:

  1. 使用 mutex 上锁来暴力上锁
mtx.lock();
count += 1;
mtx.unlock();

问题:mutex 太过重量级,他会让线程被挂起,从而需要通过系统调用,进入内核层,调度到其他线程执行,有很大的开销。 可我们只是想要修改一个小小的 int 变量而已,用昂贵的 mutex 严重影响了效率。

  1. 用更轻量级的 atomic

atomic 有专门的硬件指令加持,对它的 += 等操作,会被编译转换成专门的指令。

CPU 识别到该指令时,会锁住内存总线,放弃乱序执行等优化策略(将该指令视为一个同步点,强制同步掉之前所有的内存操作),从而向你保证该操作是原子 (atomic) 的(取其不可分割之意),不会加法加到一半另一个线程插一脚进来。 对于程序员,只需把 int 改成 atomic<int> 即可,也不必像 mutex 那样需要手动上锁解锁,因此用起来也更直观。

std::atomic<int> counter = 0;

counter += 1;

这种做法有限制:

  • count = count + 1 是不行的,不能保证原子性
  • count += 1 可以保证原子性
  • count++ 可以保证原子性

除了用方便的运算符重载之外,还可以直接调用相应的函数名fetch_add

- + \ No newline at end of file diff --git a/docs/cpp-series/type_cast.html b/docs/cpp-series/type_cast.html index 8d5f45e..6d798e3 100644 --- a/docs/cpp-series/type_cast.html +++ b/docs/cpp-series/type_cast.html @@ -9,13 +9,13 @@ - +

类型转换

类型转换是将一个数据类型的值转换为另一种数据类型的值。

C++ 中有四种类型转换:静态转换、动态转换、常量转换和重新解释转换。

静态转换(Static Cast)

静态转换是将一种数据类型的值强制转换为另一种数据类型的值。

静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。

静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。

int i = 10;
float f = static_cast<float>(i); // 静态将int类型转换为float类型

在多态情况下,将派生类指针或引用向上转换为基类的时候是安全的。

动态转换(Dynamic Cast)

动态转换通常用于将一个基类指针或引用转换为派生类指针或引用(向下转换)。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。

class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针

常量转换(Const Cast)

常量转换用于将 const 类型的对象转换为非 const 类型的对象。

常量转换只能用于转换掉 const 属性,不能改变对象的类型。

const int i = 10;
int& r = const_cast<int&>(i); // 常量转换,将const int转换为int

重新解释转换(Reinterpret Cast)

重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。

重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。

int i = 10;
float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型
- + \ No newline at end of file diff --git a/docs/cpp-series/virtual_table.html b/docs/cpp-series/virtual_table.html index eebec58..3f4a23d 100644 --- a/docs/cpp-series/virtual_table.html +++ b/docs/cpp-series/virtual_table.html @@ -9,7 +9,7 @@ - + @@ -19,7 +19,7 @@ 对于下面的代码:
#include <iostream>
#include <string>

class Father {
public:
Father ()
{
std::cout << "Father constructed !" << std::endl;
}
virtual void Func() {}
virtual void Func2(){}
void Func3() {}
};

class Mother {
public:
Mother()
{
std::cout << "Mother constructed !" << std::endl;
}
virtual void Func() {};
};

class Son : public Mother, public Father {
public:
Son() {std::cout << "Son constructed !" << std::endl;}
};

int main()
{
Father father;
std::cout << sizeof(father) << std::endl;
Mother mother;
std::cout << sizeof(mother) << std::endl;
Son son;
std::cout << sizeof(son) << std::endl;
std::cin.get();
}

输出结果是:

Father constructed !
4
Mother constructed !
4
Mother constructed !
Father constructed !
Son constructed !
8

可以发现,Father 对象和 Mother 对象占4字节,而派生类 Son 的对象占用了8个字节,这是因为:

  • 如果一个类中没有任何数据成员,那么它所占的内存是 1 字节
  • Father 和 Mother 对象中因为有虚函数,所以各自还有一个虚表指针(__vfptr),所以各自为4字节
  • 在多继承场景中,派生类会存在多个虚表指针,分别指向从 Father 和 Mother 中继承过来的虚函数,所以 son 对象占8个字节。
  1. 菱形继承

两个派生类继承同一个基类,同时两个派生类又作为基本继承给同一个派生类。这种继承形如菱形,故又称为菱形继承。

菱形继承的问题:菱形继承主要有数据冗余和二义性的问题。由于最底层的派生类继承了两个基类,同时这两个基类有继承的是一个基类,故而会造成最顶部基类的两次调用,会造成数据冗余及二义性问题。 例如:

class Person//人类
{
public :
string _name ; // 姓名
};
class Student : public Person//学生类
{
protected :
int _num ; //学号
};
class Teacher : public Person//老师类
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher//助理类
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a ;
a._name = "peter";

// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}

使用虚拟继承可以解决菱形继承的问题

虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题,其作用是在间接继承共同基类时只保留一份基类成员。 回到上面的例子,如果 Student 和 Teacher 在继承 Person 的时候使用虚拟继承,就可以解决问题。

class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{

Assistant a ;
a._name = "peter";
}

在其他地方不要去使用虚拟继承。

虚继承的实现原理是,编译器在派生类的对象中添加一个指向虚基类实例的指针,用于指向虚基类的实例。这样,在派生类中对于虚基类的成员访问都通过这个指针进行,从而保证了虚基类的唯一性。

- + \ No newline at end of file diff --git a/docs/cuda-series/CUDA_framework.html b/docs/cuda-series/CUDA_framework.html index 759a465..e1ef1ce 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 12e93e8..31f8959 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 df891a4..9b32dba 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 59193dd..8d84d51 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 09bf0f1..58b6194 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 ec674c0..67a2eaa 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 794a0c9..bc57765 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 df6f420..d023547 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 90f62ca..39f17c7 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 a1d98ff..22320bd 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 9db30ab..0ff3dea 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/rotation.html b/docs/math-series/rotation.html index 8d8f6b4..5317758 100644 --- a/docs/math-series/rotation.html +++ b/docs/math-series/rotation.html @@ -9,13 +9,13 @@ - +

旋转

怎么找到两个向量之间的旋转?

向量间平分平面上的任意一个向量都可以当做旋转轴。

rotation

可以用下面的方法找到两个向量间的最小旋转的旋转轴 uu 和旋转角 θ\theta

u=a×ba×b,θ=argcosababu=\frac{a\times b}{||a\times b||},\quad \theta = \operatorname{argcos}\frac{a\cdot b}{||a||||b||}

要怎么旋转一个向量?

可以使用多种方式表示的旋转来完成旋转:

旋转矩阵

旋转矩阵 RR 是一个正交矩阵,具有以下特点:

  • R1=RT,RTR=RRT=IR^{-1} = R^T, R^TR=RR^T=I
  • 旋转矩阵的行列式为1
  • 旋转矩阵不改变向量的长度 Rx=x||Rx||=||x||

rotationmatrix

rotationmatrixequation

虽然说旋转矩阵中有9个值,但是在考虑到 RTR=IR^TR=IdetR=1\operatorname{det}R=1 带来的约束之后,其实自由度就只剩下3了。

旋转矩阵不能使用 (1t)R0+tRt(1 - t)R_0 + tR_t 的方式来进行插值。

欧拉角

任何旋转都可以表示为围绕xyz轴(一般指本地坐标系)的旋转的组合。

但是欧拉角的组合非常多,允许xyz不同的顺序,也可以绕相同的轴转多次:XYZ, XZY, YZX, YXZ, ZYX, ZXY, XYX, XZX, YXY, YZY, ZXZ, ZYZ。

但是欧拉角存在一个万向结死锁的问题:当两个本地坐标轴旋转到了平行的状态,那么就会丢失一个自由度。

欧拉角是可以进行线性插值的。

旋转轴、角

使用一个旋转轴 uu 和一个旋转角 θ\theta 来表示旋转,具体地:

x=x+(sinθ)u×x+(1cosθ)u×(u×x)x' = x + (\operatorname{sin}\theta)u\times x + (1 - \operatorname{cos}\theta)u\times(u\times x)

旋转角可以非常方便地进行线性插值,但是在计算的时候,代码其实还是要将旋转角和旋转轴转换成矩阵。

四元数

四元数的形式为:q=xi+yj+zk+wq=xi+yj+zk+w,也可以写为:

q=[wxyz]=[wv]\boldsymbol{q}=\left[\begin{array}{l} w \\ x \\ y \\ z \end{array}\right]=\left[\begin{array}{l} w \\ v \end{array}\right]

四元数的运算:

quat

两个旋转相乘及其相关的运算:

quat2

四元数的四个元素的物理含义可以和旋转轴、角联系起来:

q=[wxyz]=[cosθ2u.xsinθ2u.ysinθ2u.zsinθ2]\boldsymbol{q}=\left[\begin{array}{l} w \\ x \\ y \\ z \end{array}\right]=\left[\begin{array}{c} \operatorname{cos}\frac{\theta}{2} \\ u.x\operatorname{sin}\frac{\theta}{2} \\ u.y\operatorname{sin}\frac{\theta}{2} \\ u.z\operatorname{sin}\frac{\theta}{2} \end{array}\right]

通过下面的公式就可以实现四元数对一个三维向量的旋转:

[0p]=q[0p]q=(q)[0p](q)\left[ \begin{array}{c} 0 \\ p' \end{array} \right] = q \left[ \begin{array}{c} 0 \\ p \end{array} \right] q^* = (-q) \left[ \begin{array}{c} 0 \\ p \end{array} \right] (-q^*)

qqq-q 表示的是同一个旋转。

对于多次旋转,有:

quat3

所以四元数是左乘,和矩阵一样。并且四元数可以线性插值。

- + \ No newline at end of file diff --git a/docs/math-series/tensor_stuff.html b/docs/math-series/tensor_stuff.html index b3eaabe..6948bce 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 9784e40..f2fe46a 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 c711ee1..50da2f7 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/animnode.html b/docs/unreal-series/animnode.html index 90b3b8b..db8fa65 100644 --- a/docs/unreal-series/animnode.html +++ b/docs/unreal-series/animnode.html @@ -9,13 +9,13 @@ - +
-

动画节点

动画节点在动画蓝图中用于执行多种操作,例如处理动画 Pose、混合动画姿势以及操控骨骼网格体的骨骼。

::: 动画蓝图

动画蓝图是动画实例(AnimInstance)的子类。可以这么理解,动画蓝图就是动画实例的可视化脚本。我们可以通过编辑动画蓝图来编写动画实例的逻辑。

:::

一个完整的动画节点包括两个基本组件:

  • 一个运行时结构体(AnimNode),用于执行生成输出 Pose 所需的实际计算
  • 一个编辑器容器类(AnimGraphNode),负责处理图标节点的视觉表现和功能,例如节点标题和上下文菜单

运行时节点组件

运行时结构体是一种结构体,派生自 FAnimNode_Base 类,负责初始化、更新以及在一个或多个输入姿势上执行操作来生成所需的输出姿势。它还会声明节点为执行所需操作需具备的输入姿势链接和属性。

一般来说需要:

  1. Pose 输入

Pose 输入一般是通过创建 FPoseLink 或 FComponentSpacePoseLink 类型的属性来公开为一个Pin。其中 FPoseLink 用于处理 Local Space 的 Pose 时使用,例如混合动画。FComponentSpacePoseLink 在处理 Component Space 中的 Pose 时使用。例如:

animnode

一个节点还可以有多个 Pose 输入。另外,这两种类型的属性只能公开为输入引脚,无法被隐藏或者仅作为 Details 面板中的可编辑属性。

  1. 属性和数据输入

可以通过 UPROPERTY 宏来声明自定义属性:

UPROPERTY(Category=Settings, meta(PinShownByDefault))
mutable float Alpha;

使用特殊的 meta,动画节点属性可以公开为数据输入引脚,以允许值传递到节点:

  • NeverAsPin:将属性作为AnimGraph中的数据引脚隐藏,并且仅可在节点的 细节(Details) 面板中编辑
  • PinHiddenByDefault:将属性作为引脚隐藏。但是可以通过 Details 面板设置,将属性作为数据引脚在AnimGraph中公开
  • PinShownByDefault:将属性作为数据引脚在AnimGraph中公开
  • AlwaysAsPin:始终将属性作为数据点在AnimGraph中公开

编辑器节点组件

编辑器类派生自 UAnimGraphNode_Base,负责节点标题等视觉元素或添加上下文菜单操作。编辑器类应该包含一个公开为可编辑的运行时节点实例:

UPROPERTY(Category=Settings)
FAnimNode_ApplyAdditive Node;

动画节点的运作

动画蓝图是以节点树的方式来组织动画的运行逻辑的。在运作的过程中,主要需要了解三个名词:

  1. 更新(Update)

节点的Update用于根据Weight计算动画的各种权重。因为Weight会在下一阶段清空。

如果按照Epic的编写习惯,我们应该在Update里面拿到所有外部数据并且预计算,保证Evaluate可以直接使用。

  1. 评估(Evaluate)

根据上一个节点的Pose,计算出输出到下个节点的Pose。

这是动画节点最重要的部分。正常来说我们应该把骨骼计算部分都放在这里。

注意Update和Evaluate都有可能运行在子线程上,除了读写AnimInstanceProxy外,操作其他东西都不是线程安全的,尽可能不要碰外部的UObject。

  1. 根节点(Root)

也叫 OutPut Pose 节点。根节点是最重要的节点。对于用户来说,他是所有动画逻辑的输出节点。但是对于蓝图来说,他是整个蓝图节点的开始。AnimInstance 将从这里开始建立整个动画节点的树状联系。

以下面的蓝图举例:

work

  1. 在 Update 的时候,执行 Root 的 Update
  2. Root 找到连接到他的上一个节点,BlendPosesByBool 节点,执行他的 Update
  3. BlendPosesByBool 节点的 Update 首先会读取所有 Pin 的值,这个节点来说主要是 Active Value
  4. 然后根据 Value,执行对应节点的 Update,也就是上一个节点,BlendSpace Player节点的 Update
  5. BlendSpacePlayer 的 Update 会读取 Speed 的值到属性里
  6. 然后是 Evaluate,沿着同样的线路
  7. 首先执行 Output 的 Evaluate,他会先执行上一个节点的 Evaluate
  8. 也就是 BlendPosesByBool 的 Evaluate,这里他也会先执行当前激活的节点的 Evaluate,也就是 BlendSpacePlayer 的 Evaluate
  9. BlendSpacePlayer 的 Evaluate 很简单,根据输入的参数,从 BlendSpace 里面输出一个混合的 Pose
  10. 然后回到 BlendPosesByBool,如果有混合时间而且处于混合状态,他会把两个 Pose 按照比例混合再输出,如果没有,则直接原样输出其中一个 Pose,当前是 BlendSpace 的输出
  11. 最后的Output节点把前面的Pose输出,大功告成
- +

动画节点

动画节点在动画蓝图中用于执行多种操作,例如处理动画 Pose、混合动画姿势以及操控骨骼网格体的骨骼。

动画蓝图

动画蓝图是动画实例(AnimInstance)的子类。可以这么理解,动画蓝图就是动画实例的可视化脚本。我们可以通过编辑动画蓝图来编写动画实例的逻辑。

一个完整的动画节点包括两个基本组件:

  • 一个运行时结构体(AnimNode),用于执行生成输出 Pose 所需的实际计算
  • 一个编辑器容器类(AnimGraphNode),负责处理图标节点的视觉表现和功能,例如节点标题和上下文菜单

运行时节点组件

运行时结构体是一种结构体,派生自 FAnimNode_Base 类,负责初始化、更新以及在一个或多个输入姿势上执行操作来生成所需的输出姿势。它还会声明节点为执行所需操作需具备的输入姿势链接和属性。

一般来说需要:

  1. Pose 输入

Pose 输入一般是通过创建 FPoseLink 或 FComponentSpacePoseLink 类型的属性来公开为一个Pin。其中 FPoseLink 用于处理 Local Space 的 Pose 时使用,例如混合动画。FComponentSpacePoseLink 在处理 Component Space 中的 Pose 时使用。例如:

animnode

一个节点还可以有多个 Pose 输入。另外,这两种类型的属性只能公开为输入引脚,无法被隐藏或者仅作为 Details 面板中的可编辑属性。

  1. 属性和数据输入

可以通过 UPROPERTY 宏来声明自定义属性:

UPROPERTY(Category=Settings, meta(PinShownByDefault))
mutable float Alpha;

使用特殊的 meta,动画节点属性可以公开为数据输入引脚,以允许值传递到节点:

  • NeverAsPin:将属性作为AnimGraph中的数据引脚隐藏,并且仅可在节点的 细节(Details) 面板中编辑
  • PinHiddenByDefault:将属性作为引脚隐藏。但是可以通过 Details 面板设置,将属性作为数据引脚在AnimGraph中公开
  • PinShownByDefault:将属性作为数据引脚在AnimGraph中公开
  • AlwaysAsPin:始终将属性作为数据点在AnimGraph中公开

编辑器节点组件

编辑器类派生自 UAnimGraphNode_Base,负责节点标题等视觉元素或添加上下文菜单操作。编辑器类应该包含一个公开为可编辑的运行时节点实例:

UPROPERTY(Category=Settings)
FAnimNode_ApplyAdditive Node;

动画节点的运作

动画蓝图是以节点树的方式来组织动画的运行逻辑的。在运作的过程中,主要需要了解三个名词:

  1. 更新(Update)

节点的Update用于根据Weight计算动画的各种权重。因为Weight会在下一阶段清空。

如果按照Epic的编写习惯,我们应该在Update里面拿到所有外部数据并且预计算,保证Evaluate可以直接使用。

  1. 评估(Evaluate)

根据上一个节点的Pose,计算出输出到下个节点的Pose。

这是动画节点最重要的部分。正常来说我们应该把骨骼计算部分都放在这里。

注意Update和Evaluate都有可能运行在子线程上,除了读写AnimInstanceProxy外,操作其他东西都不是线程安全的,尽可能不要碰外部的UObject。

  1. 根节点(Root)

也叫 OutPut Pose 节点。根节点是最重要的节点。对于用户来说,他是所有动画逻辑的输出节点。但是对于蓝图来说,他是整个蓝图节点的开始。AnimInstance 将从这里开始建立整个动画节点的树状联系。

以下面的蓝图举例:

work

  1. 在 Update 的时候,执行 Root 的 Update
  2. Root 找到连接到他的上一个节点,BlendPosesByBool 节点,执行他的 Update
  3. BlendPosesByBool 节点的 Update 首先会读取所有 Pin 的值,这个节点来说主要是 Active Value
  4. 然后根据 Value,执行对应节点的 Update,也就是上一个节点,BlendSpace Player节点的 Update
  5. BlendSpacePlayer 的 Update 会读取 Speed 的值到属性里
  6. 然后是 Evaluate,沿着同样的线路
  7. 首先执行 Output 的 Evaluate,他会先执行上一个节点的 Evaluate
  8. 也就是 BlendPosesByBool 的 Evaluate,这里他也会先执行当前激活的节点的 Evaluate,也就是 BlendSpacePlayer 的 Evaluate
  9. BlendSpacePlayer 的 Evaluate 很简单,根据输入的参数,从 BlendSpace 里面输出一个混合的 Pose
  10. 然后回到 BlendPosesByBool,如果有混合时间而且处于混合状态,他会把两个 Pose 按照比例混合再输出,如果没有,则直接原样输出其中一个 Pose,当前是 BlendSpace 的输出
  11. 最后的Output节点把前面的Pose输出,大功告成
+ \ No newline at end of file diff --git a/docs/unreal-series/bone-anim.html b/docs/unreal-series/bone-anim.html index 9d358de..89e015c 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类的成员函数,该函数会在每一帧被调用,以计算对应的组件在这一帧中的行为。

UE 中作为骨骼动画载体的是 SkeletalMeshComponent,SkeletalMeshComponent 继承自 SkinnedMeshComponent。在 SkeletalMeshComponent 创建时,会创建一个 AnimationInstance,这就是主要负责计算最终 Pose 的对象,而我们制作的 AnimationBlueprint (动画蓝图)也是基于 UAnimationInstance 这个类的。(目前 AnimationInstance 的逻辑慢慢地开始迁往 AnimInstanceProxy,目的是动画系统的多线程优化)

在 SkeletalMeshComponent 进行 Tick 时,会调用 TickAnimation 方法,然后会调用 AnimationInstance 的 UpdateAnimation() 方法,此方法会调用一遍所有动画蓝图中连接的节点的 Update_AnyThread() 方法,用来更新所有节点的状态。

然后后续根据设置的不同会从 Tick 函数或者 Work 线程中调用 SkeletalMeshComponent 的 RefreshBoneTransforms() 方法,此方法进而会调用动画蓝图所有节点的 Evaluate_AnyThread() 方法。Evaluate 就是实际计算出所有骨骼的 Transform 信息的步骤。计算得到的 Pose 最终会给到渲染线程,并且存在 SkeletalMeshComponent 上。

Framework

初始化

动画蓝图的初始化要从SkeletalMeshComponent的注册开始, 会调用到InitAnim()函数。函数中主要做了这么几件事:

  1. ClearAnimScriptInstance(); 清理之前的 AnimInstance 对象
  2. RecalcRequiredBones(); 根据Lod计算特定的骨骼信息, 数据来自 FSkeletalMeshRenderData, 然后如果有物理资产 PhysicsAsset 会刷新 PhysAssetBones
  3. InitializeAnimScriptInstance(); 创建指定的动画蓝图实例对象
  4. 如果符合条件,会执行一次 TickAnimation()RefreshBoneTransforms()

动画更新

首先我们需要知道, 动画更新默认是多线程的。以下讨论都基于多线程动画更新:

SkeletalMeshComponent

动画更新要从 SkeletalMeshComponent::TickComponent()开始, SkeletalMeshComponent相对于其父类USkinnedMeshComponent, 在Tick中额外多了一些布料和物理相关的一部分逻辑。

Tick 中主要执行了:

  • TickPose() :要作用就是刷新动画蓝图和相关Node的数据, 为后面的骨骼更新做准备
  • RefreshBoneTransforms():作用就是刷新骨骼数据(通过调用 ParallelEvaluateAnimation() 来并行执行 Evaluate 任务, 然后提供给骨骼模型做最终的渲染

AnimNode

主要看一下 FAnimNode_Base 里的几个多线程虚函数:

virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context);
virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context);
virtual void Update_AnyThread(const FAnimationUpdateContext& Context);
virtual void Evaluate_AnyThread(FPoseContext& Output);
virtual void EvaluateComponentSpace_AnyThread(FComponentSpacePoseContext& Output);

Initialize_AnyThread

进行初始化,会在很多地方调用到, 比如编译以后也会调用一次, 在 USkeletalMeshComponent::OnRegister 时也会通过 Proxy 调用 InitializeRootNode_WithRoot() 一路初始化, 还有状态机 SetState() 时一路通过 LinkedNode 找到每个节点进行初始化等等。

CacheBones_AnyThread

在骨骼信息发生变换的时候引用到, 比如LOD变化时就会调用到. 主要用于刷新该节点所引用的骨骼索引。

Update_AnyThread

这个调用就比较频繁了, 在 TickPose 和 RefreshBoneTransform 的过程中都可能被调用。

这个函数通常用来对刷新骨骼位置所需要的变量进行计算。

Evaluate_AnyThread

用来计算并刷新 Local 空间的骨骼数据的函数, 一般我们自定义动画节点通常会重写这个函数大作文章。

EvaluateComponentSpace_AnyThread

同上, 但是是在组件空间的, 两者只选其一即可。

- + \ No newline at end of file diff --git a/docs/unreal-series/bounds.html b/docs/unreal-series/bounds.html index 829b608..c2d0841 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/collision.html b/docs/unreal-series/collision.html index d715784..2cc2cbe 100644 --- a/docs/unreal-series/collision.html +++ b/docs/unreal-series/collision.html @@ -9,14 +9,14 @@ - +

    UE的碰撞与检测

    碰撞检测

    碰撞检测设置

    在 UE 中,每个参与碰撞的物体都包含了两个属性:碰撞通道(Object/Trace Channel)以及其与其他 Channel 之间的交互类型,交互类型可以分为三类:

    • Ignore-可穿透且无任何事件通知
    • Overlap-可穿透且有事件通知
    • Block-不可穿透且有事件通知

    其中交互类型是一张表,在 UE 中叫做 Preset,其中记录了当前这个物体和各种通道的物体之间是Ignore、Overlap还是Block。

    发生碰撞的两个物体,只要有一个物体将碰撞对向的类型设置为 Ignore,那么就不会触发任何事件和物理现象。如果两个碰撞对象都互相将对方设置为 Block 时,就会产生物理上的阻隔。其他情况下则都是 Overlap,仅会产生事件,但是不会阻隔。

    另外,Preset 中还有一个选项以确认启用碰撞的类型,有四类:

    • No Collision(没有碰撞):在物理引擎中此形体将不具有任何表示。不可用于空间查询(光线投射、Sweep、重叠)或模拟(刚体、约束)。此设置可提供最佳性能,尤其是对于移动对象。
    • Query Only(仅查询无物理碰撞):此形体仅可用于空间查询(光线投射、Sweep和重叠)。不可用于模拟(刚体、约束)。对于角色运动和不需要物理模拟的对象,此设置非常有用。通过缩小物理模拟树中的数据来实现一些性能提升。
    • Physics Only(仅碰撞无物理查询):此形体仅可用于物理模拟(刚体、约束)。不可用于空间查询(光线投射、Sweep、重叠)。对于角色上不需要按骨骼进行检测的模拟次级运动,此设置非常有用。通过缩小查询树中的数据来实现一些性能提升。
    • Collision Enabled(启用碰撞物理和查询):此形体可用于空间查询(光线投射、Sweep、重叠)和模拟(刚体、约束)。

    碰撞检测方法

    UE 中的碰撞检测主要分为三层:

    • World 层:最靠近业务需求的层次,已搭建好的物理系统环境,并提供一系列可直接调用的接口
    • Interface 层:也是接口封装层,对下封装好第三方物理插件,并负责搭建好物理系统环境
    • Plugin 层:具体的物理引擎插件,例如 UE4 的 PhysX、UE5 的 Chaos 等等

    主要使用的就是 World 层的接口。

    1. 射线碰撞检测

    UE中的射线查询主要使用 UWorld::LineTraceSingleByChannel 和 UWorld::LineTraceSingleByObjectType 函数进行。 其中前者通过 Trace Channel 进行查询,后者通过 Object Type(也就是 Object Channel)进行查询。

    除了打射线,还可以打具有形状的射线(Sphere、Box、Capsule等等)。

    1. 重叠碰撞检测

    一般使用 UWorld::OverlapMultiByChannel 函数。函数中要求输入一个 FCollisionShape,FCollisionShape 一般支持 Sphere、Capsule、Box 和 Line,个人一般用Box。函数能够检测得到与该 CollisionShape 重叠的碰撞物,以 FOverlapResult 的形式返回。

    检测到以后,那么就可以通过 FHitResult/FOverlapResult 的 GetComponent() 函数拿到碰撞体的 PrimitiveComponent。再通过 PrimitiveComponent 的 GetBodyInstance 函数拿到碰撞体的实例 BodyInstance。

    碰撞体

    我们可以从外部导入 Mesh 的碰撞体,也可以在 UE 中为导入的 Mesh 自动生成可用于游戏的碰撞体。这种自动生成的碰撞体分为两种,简单碰撞体和复杂碰撞体。

    简单碰撞体

    简单碰撞体就是用基础形状(Box、Sphere、Capsule、Convex Shape 等等)来定义物体边界的 Collision Mesh。除了这些基本形状,还有一种名为KDOP(K Discrete Oriented Polytope,K-离散有向多面体)的简单碰撞体。这种碰撞体会生成K组轴对称的平面,并将其形状尽可能地贴近所选的Mesh。

    simple

    另外还可以使用凸包分解(Auto Convex Collision)来生成凸包体碰撞体。

    除了在引擎重生成,还可以从外部导入自定义碰撞体,这个时候需要遵循一定的命名规范:

    • UBX_[Mesh Name]: Box 碰撞体
    • USP_[Mesh Name]: Sphere 碰撞体
    • UCX_[Mesh Name]: Convex Shape 碰撞体

    将这些符合命名规范的碰撞体和 Mesh 一起导出为 FBX,然后导入到引擎中。导入后,UE 会找到碰撞体,将其与实际的 Mesh 分离,并转换为 Collision Mesh。

    复杂碰撞体

    复杂碰撞体即该 Mesh 本身。在 StaticMesh 编辑器的细节面板中,我们可以在 Collision->Collision Complexity 中对引擎生成碰撞体的复杂度进行设置,以便进行不同精度的碰撞查询,共有四个选项:

    • Project Default-项目默认设置,在 Project Setting->Engine->Physics->Simulation 中可以找到
    • Simple And Complex-同时生成简单碰撞体和复杂碰撞体。简单碰撞体用于常规的场景查询和碰撞测试;复杂碰撞体用于复杂场景查询。
    • Use Simple Collision As Complex-仅创建简单碰撞体,用于场景查询以及碰撞测试。
    • Use Complex Collision As Simple-仅创建复杂碰撞体,用于场景查询以及碰撞测试。但此时无法对其进行模拟。
    - + \ No newline at end of file diff --git a/docs/unreal-series/resource.html b/docs/unreal-series/resource.html index 223902e..47648a5 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 b597141..7443081 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 041e698..c4296ab 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