[{"content":"题目：两数之和\n最容易想到的方法就是两层for循环，时间复杂度为O(n^2)\n但是可以利用哈希表O(1)的查找时间复杂度将其降至 O(n)\n具体实现：\n在循环中对”表内是否存在等于‘目标值减去当前值’的数进行判断“（因为需要进行配对），如果没有，就将当前的元素加入到哈希表，否则直接输出\n注：这里的判断条件——it != um.end() 的意思是找到了匹配的数字，因为如果哈希表没有找到对应的key时，会返回尾地址\n1vector\u0026lt;int\u0026gt; twoSum(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 2{ 3 std::unordered_map\u0026lt;int,int\u0026gt; um; 4 for(int i = 0; i \u0026lt; nums.size(); i++) 5 { 6 auto it = um.find(target - nums[i]); 7 if(it != um.end()) return {it-\u0026gt;second, i}; 8 um[nums[i]] = i; 9 } 10 return {}; 11} ","permalink":"https://jiat-t.github.io/leetcode/hash/easy/two-sum/","summary":"两数之和的解题思路与 C++ 实现。","title":"两数之和"},{"content":"题目：两数相加\n首先明确思路，逐节点的将l1和l2相加，如果遇到需要进位的情况，就单独将所进的位数保存起来，并加到下一次的sum中去，然后在同一次循环中构造好相加后的链表\n1ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 2{ 3 ListNode dummy(0); 4 ListNode* curr = \u0026amp;dummy; 5 6 int carry = 0; 7 while(l1 || l2 || carry != 0) 8 { 9 int sum = carry; 10 if(l1) 11 { 12 sum += l1-\u0026gt;val; 13 l1 = l1-\u0026gt;next; 14 } 15 if(l2) 16 { 17 sum += l2-\u0026gt;val; 18 l2 = l2-\u0026gt;next; 19 } 20 21 // 进位 22 carry = sum / 10; 23 int digit = sum % 10; 24 curr-\u0026gt;next = new ListNode(digit); 25 curr = curr-\u0026gt;next; 26 } 27 return dummy.next; 28} ","permalink":"https://jiat-t.github.io/leetcode/list/medium/add-two-numbers/","summary":"两数相加的解题思路与 C++ 实现。","title":"两数相加"},{"content":"题目：无重复字符的最长子串\n用到的是滑动窗口和unorder_map,\n通过两个条件限制窗口范围：\n1.当（left，right）存在重复字符时，那么（left，right+1\u0026hellip;..right+n）都存在重复数值\n2.当（left，right）不存在重复字符时，那么（left+1\u0026hellip;.left+n，right）都不存在重复字符\n因此，（left，right+1\u0026hellip;..right+n）与（left+1\u0026hellip;.left+n，right）都不需要再去进行遍历\n核心：用unordered_map记录每个字符的出现次数\n1 2int lengthOfLongestSubstring(string s) 3 4{ 5 6 int maxStr = 0; // 用来记录出现过的“最长”无重复子串的长度。 7 8 9 10 // 这里的 um (unordered_map) 是核心工具。 11 12 // Key (char): 窗口里的字符 13 14 // Value (int): 这个字符在当前窗口里出现了几次 15 16 std::unordered_map\u0026lt;char, int\u0026gt; um; 17 18 19 20 // 开始滑动窗口。一开始，左右边界都在最左边（索引 0）。 21 22 // right++ 代表窗口的右边缘在不断向右扩展，吞进新的字符。 23 24 for(int left = 0, right = 0; right \u0026lt; s.size(); right++) 25 26 { 27 28 // s[right] 是刚刚进入窗口的新字符。 29 30 // um[s[right]]++ 的意思是：让这个新字符的出现次数 +1。 31 32 um[s[right]]++; 33 34 35 36 // 检查刚刚吞进来的字符，是不是导致窗口里有重复了？ 37 38 // 如果 \u0026gt; 1，说明这个字符之前已经在窗口里存在了。 39 40 while(um[s[right]] \u0026gt; 1) 41 42 { 43 44 // 既然有重复了，就缩小窗口： 45 46 // 把最左边的字符 s[left] 踢出窗口，所以它的出现次数 -1。 47 48 um[s[left]]--; 49 50 // 左边界向右移动一格，窗口缩小。 51 52 left++; 53 54 } 55 56 57 58 // 此时认为窗口里已经没有重复字符了。 59 60 // right - left + 1 就是当前窗口的长度。 61 62 // 比如 left=0, right=2，长度就是 2 - 0 + 1 = 3。 63 64 // 用 std::max 更新历史最大长度。 65 66 maxStr = std::max(maxStr, right - left + 1); 67 68 } 69 70 return maxStr; // 遍历完整个字符串，返回找到的最大值。 71 72} ","permalink":"https://jiat-t.github.io/leetcode/sliding-window/medium/longest-substring-without-repeating-characters/","summary":"无重复字符的最长子串的解题思路与 C++ 实现。","title":"无重复字符的最长子串"},{"content":"题目：最长回文子串\n笔记 1 使用的是“中心扩散法”\n参考了这里的解法：https://leetcode.cn/problems/longest-palindromic-substring/solutions/2958179/mo-ban-on-manacher-suan-fa-pythonjavacgo-t6cx/\n对于奇回文串，我们从最中间的一个元素开始向两边扩散，比如“bab”，第一次判断时 left = right，条件符合，所以 left\u0026ndash;， right++；然后进入第二次判断，此时两者指向的字符都是‘b\u0026rsquo;，条件依然满足\u0026hellip;.直到左右指针指向的字符不同时，计算当前字符串长度\n对于偶回文串，还是从中间开始扩散，只不过这次需要选取两个元素\n为了将两种情况合并处理，可以将 i 的最大范围改为 2n - 1，因为\n奇数长度回文中心：每个字符本身就是一个中心 → n 个 偶数长度回文中心：每两个相邻字符之间是一个中心 → n - 1 个 两者相加，所有回文中心的数量就是 2n - 1 个\nlongestPalindrome（）\n1string longestPalindrome(string s) 2{ 3 int left = 0, right = 0; 4 int n = s.size(); 5 for(int i = 0; i \u0026lt; 2 * n - 1; i++) 6 { 7 int l = i / 2, r = (i + 1) / 2; 8 while(0 \u0026lt;= l \u0026amp;\u0026amp; r \u0026lt; n \u0026amp;\u0026amp; s[l] == s[r]) 9 { 10 l--; 11 r++; 12 } 13 if(r - l - 1 \u0026gt; right - left) 14 { 15 right = r; 16 left = l + 1; 17 } 18 } 19 return s.substr(left, right - left); 20} 来源：语雀原笔记\n笔记 2 以一个字符（奇数回文串）或两个字符中间的空隙（偶数回文串）为中心，向左右两端扩散，如果两端字符相同，则继续扩展，并记录当前字符串的起始点\nlongestPalindrome(）\n1std::pair\u0026lt;int, int\u0026gt; ExpandAroundCenter(const string\u0026amp; s, int left, int right) 2{ 3 while(left \u0026gt;= 0 \u0026amp;\u0026amp; right \u0026lt; s.size() \u0026amp;\u0026amp; s[left] == s[right]) 4 { 5 --left; 6 ++right; 7 } 8 return {left+1, right-1}; 9} 10string longestPalindrome(string s) 11{ 12 int left = 0, right = 0; 13 // 如果输入字符串总长为1，则直接返回该字符串 14 if(s.size() \u0026lt; 2) 15 return s; 16 for(int i = 0; i \u0026lt; s.size(); ++i) 17 { 18 // 奇数回文串 19 auto [left1, right1] = ExpandAroundCenter(s, i, i); 20 // 偶数回文串 21 auto [left2, right2] = ExpandAroundCenter(s, i, i+1); 22 // 判断是奇数还是偶数 23 if(right1 - left1 \u0026gt; right - left) 24 { 25 left = left1; 26 right = right1; 27 } 28 if(right2 - left2 \u0026gt; right - left) 29 { 30 left = left2; 31 right = right2; 32 } 33 } 34 // substr（start，length）：提取特定字符串 35 return s.substr(left, right-left+1); 36} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/double-pointers/medium/longest-palindromic-substring/","summary":"最长回文子串的解题思路与 C++ 实现。","title":"最长回文子串"},{"content":"题目：整数反转\n通过对输入数字模10得到最低位的数字，然后将输入数字除10，减少输入数字的一位位数，使用此数字进行下一轮循环，直到输入数字最终等于零\n注意：在循环过程中，可能出现溢出的情况，因此需要在每一次循环开始前对已经反转的数字进行范围判断，如果已经溢出，则返回零\nreverse()\n1int reverse(int x) 2 { 3 int rev = 0; 4 while(x != 0) 5 { 6 if(rev \u0026lt; INT_MIN/10 || rev \u0026gt; INT_MAX/10) return 0; 7 int d = x % 10; 8 x /= 10; 9 rev = rev * 10 + d; 10 } 11 return rev; 12 } 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/string/medium/reverse-integer/","summary":"整数反转的解题思路与 C++ 实现。","title":"整数反转"},{"content":"题目：字符串转换整数 (atoi)\n相关笔记：整数反转\n根据题目的要求逐步实现函数功能：\n1.空格：读入字符串并丢弃前导空格\u0026ndash;在循环外定义字符串下标，通过while循环判断前面的字符是否为“ ”，如果是，则i加一并进入下一次是否为空格的判断\n2.符号：检查下一个字符为“-”还是“+”\u0026ndash;通过if判断之后的字符有无符号，有则将sign标记为-1或1（在得出了最后的数字之后乘上），没有则默认为1\n3.转换：跳过前置零，直至最后一个字符\u0026ndash;这里的零不用单独写一条if语句来判断，可以通过与上一题相同的方法来解决（rev = rev *10 + d）。要注意的是，string使用【】得出的是char类型的字符，而不是整数，需要减去‘0’才能变为int类型\n4.舍入：范围 [−231, 231 − 1]\u0026ndash;在进行转换之前需要先对上一次的计算结果进行范围判断，如果大于INT_MAX/10或等于INT_MAX/10但是下一位大于7，则直接返回上一次的值（原因：如果上一次的值已经大于了INT_MAX/10，那么经过这次的循环之后，必然会超出范围；又因为最大值是231 − 1，因此在等于INT_MAX的情况下，个位最大值只能是7）\nmyAtoi()\n1int myAtoi(string s) 2 { 3 int i = 0, result = 0, sign = 1; 4 while(i \u0026lt; s.size() \u0026amp;\u0026amp; s[i] == \u0026#39; \u0026#39;) i++; 5 if(i == s.size()) return 0; 6 if(s[i] == \u0026#39;-\u0026#39; || s[i] == \u0026#39;+\u0026#39;) 7 { 8 sign = s[i] == \u0026#39;-\u0026#39; ? -1 : 1; 9 i++; 10 } 11 while(i \u0026lt; s.size() \u0026amp;\u0026amp; s[i] \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= \u0026#39;9\u0026#39;) 12 { 13 int digit = s[i] - \u0026#39;0\u0026#39;; 14 if(result \u0026gt; INT_MAX / 10 || (result == INT_MAX / 10 \u0026amp;\u0026amp; digit \u0026gt; 7)) 15 return sign == 1 ? INT_MAX : INT_MIN; 16 result = result * 10 + digit; 17 i++; 18 } 19 return result * sign; 20 } 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/string/medium/string-to-integer-atoi/","summary":"字符串转换整数 (atoi)的解题思路与 C++ 实现。","title":"字符串转换整数 (atoi)"},{"content":"题目：回文数\n因为负数不可能为回文数，所以直接排除\n之后就和之前的题目一样，把反转后的数字再与原数字比较就行\n（注意：这里使用int会报错\u0026ndash;超出了类型范围，所以改用了unsigned int，因为负数在第一关就被筛掉了）\nisPalindrome()\n1bool isPalindrome(int x) 2 { 3 if(x \u0026lt; 0) return false; 4 unsigned int result = 0, x_copy = x; 5 while(x != 0) 6 { 7 unsigned int a = x % 10; 8 x /= 10; 9 result = result * 10 + a; 10 } 11 if(result == x_copy) 12 return true; 13 return false; 14 } 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/technique/easy/palindrome-number/","summary":"回文数的解题思路与 C++ 实现。","title":"回文数"},{"content":"题目：盛最多水的容器\n如果直接用两个for循环遍历所有容积情况，时间复杂度是O(n^2)，会超时\u0026hellip;..\n所有需要使用正确的算法\n那么这题使用的就是双指针的方法。\n首先分析思路：虽然说是双指针，但实际上使用的是两个索引的方法\u0026ndash;定义left=0（最左边），right=数组大小-1（最右边），然后计算出初始的容积大小\n我们发现，移动长板（向里走，也就是将索引向里挪一位）会使水槽的宽度会变窄，即使移动过后得到的板子是更长的板子，容积仍然会变小，更短的板子同理（短板效应）；但是如果移动的是短板，假如下一块板子是更长的，那么就有可能会使容积变大（e.g.第一块板子长为1，最后一块板子长为7，一共九块板子，那么此时容积就是（1x8）=8，接下俩移动短板，第二块板子长为8，此时容积为（7x7）=49，容积变大了）\n所以，只需要找到两块板子中更短的那块，固定住长板，只移动短板就行\n1 2int maxArea(vector\u0026lt;int\u0026gt;\u0026amp; height) 3 4 { 5 6 int left = 0, right = static_cast\u0026lt;int\u0026gt;(height.size()) - 1; 7 8 int volumn = std::min(height[left],height[right]) * right; 9 10 while(left \u0026lt; right) 11 12 { 13 14 if(height[left] \u0026lt; height[right]) 15 16 { 17 18 left++; 19 20 } 21 22 else 23 24 { 25 26 right--; 27 28 } 29 30 volumn = std::max(volumn, (std::min(height[left],height[right]) * (right - left))); 31 32 } 33 34 return volumn; 35 36 } ","permalink":"https://jiat-t.github.io/leetcode/double-pointers/medium/container-with-most-water/","summary":"盛最多水的容器的解题思路与 C++ 实现。","title":"盛最多水的容器"},{"content":"题目：整数转罗马数字\n使用到的是类似哈希表的结构\n当当前数字大于此时values中存储的值时，认为其满足“如果该值不是以 4 或 9 开头，请选择可以从输入中减去的最大值的符号，将该符号附加到结果，减去其值，然后将其余部分转换为罗马数字”的条件，需要找到此时value对应的key，拼接到res的末尾\n注意：在这里，我们将4，9，10等特殊值直接存储到哈希表结构中，因为可以原算法就是通过顺序遍历的方法进行对应值的筛选\nintToRoman（）\n1string intToRoman(int num) 2 { 3 std::array\u0026lt;int, 13\u0026gt; values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}; 4 std::array\u0026lt;std::string, 13\u0026gt; keys = {\u0026#34;M\u0026#34;, \u0026#34;CM\u0026#34;, \u0026#34;D\u0026#34;, \u0026#34;CD\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;XC\u0026#34;, \u0026#34;L\u0026#34;, \u0026#34;XL\u0026#34;, \u0026#34;X\u0026#34;, \u0026#34;IX\u0026#34;, \u0026#34;V\u0026#34;, \u0026#34;IV\u0026#34;, \u0026#34;I\u0026#34;}; 5 std::string res = \u0026#34;\u0026#34;; 6 for(int i = 0; i \u0026lt; 13; i++) 7 { 8 while(values[i] \u0026lt;= num) 9 { 10 num -= values[i]; 11 res += keys[i]; 12 } 13 } 14 return res; 15 } 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/hash/medium/integer-to-roman/","summary":"整数转罗马数字的解题思路与 C++ 实现。","title":"整数转罗马数字"},{"content":"题目：罗马数字转整数\n同样使用到了哈希表\n不同的是，这次并没有将4，9等特殊数字存储进来，而是选择在后面的分支中进行处理：如果下一个字符代表的数字大于当前字符，那么就减去当前字符代表的数字（如：此次读取到了I，而下一个字符为V，也就是说这两个字符本来是连在一起的（IV：4），这是就需要减去1，而5会在下一次循环中加上去）\nromanToInt（）\n1class Solution 2{ 3public: 4 unordered_map\u0026lt;char, int\u0026gt; symbolValues = 5 { 6 {\u0026#39;I\u0026#39;, 1}, 7 {\u0026#39;V\u0026#39;, 5}, 8 {\u0026#39;X\u0026#39;, 10}, 9 {\u0026#39;L\u0026#39;, 50}, 10 {\u0026#39;C\u0026#39;, 100}, 11 {\u0026#39;D\u0026#39;, 500}, 12 {\u0026#39;M\u0026#39;, 1000}, 13 }; 14 15 int romanToInt(string s) 16 { 17 int res = 0; 18 int n = s.size(); 19 for(int i = 0; i \u0026lt; n; i++) 20 { 21 int currValue = symbolValues[s[i]]; 22 if (i \u0026lt; n - 1 \u0026amp;\u0026amp; currValue \u0026lt; symbolValues[s[i + 1]]) 23 res -= currValue; 24 else 25 res += currValue; 26 } 27 return res; 28 } 29}; 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/hash/easy/roman-to-integer/","summary":"罗马数字转整数的解题思路与 C++ 实现。","title":"罗马数字转整数"},{"content":"题目：最长公共前缀\n思路：从第一个元素的第一个字符开始，先逐渐向后遍历，检查第一个字符是否都相同，然后再进行第二个字符的遍历\u0026hellip;..直到出现不同的字符，然后返回公共前缀\n结尾的“return strs【0】”代表着：如果循环正常结束，就说明第一个元素就是公共的前缀\nlongestCommonPrefix（）\n1string longestCommonPrefix(vector\u0026lt;string\u0026gt;\u0026amp; strs) 2 { 3 if(strs.empty()) return \u0026#34;\u0026#34;; 4 5 for(int i = 0; i \u0026lt; strs[0].length(); i++) 6 { 7 for(int j = 0; j \u0026lt; strs.size(); j++) 8 { 9 if(i == strs[j].length() || strs[j][i] != strs[0][i]) 10 return std::string(strs[0].substr(0, i)); 11 } 12 } 13 return strs[0]; 14 } 原文：LeetCode\n","permalink":"https://jiat-t.github.io/leetcode/string/easy/longest-common-prefix/","summary":"最长公共前缀的解题思路与 C++ 实现。","title":"最长公共前缀"},{"content":"题目：最接近的三数之和\n与上一题（15题）一样，使用的是双指针与双循环，这样可以将O(n^3)的时间复杂度降低至O(n^2)\n首先对数组进行排序，方便后续左右指针的移动\n如果当前的和恰好与目标值相同，就直接返回\n接着与最接近的值进行比较，如果当前值更加接近，则替换\n最后判断当前的和与目标值的大小关系，如果更小，说明需要找到更大的数，因此将左指针右移，反之，将右指针左移\nthreeSumClosest（）\n1int threeSumClosest(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 2 { 3 int length = nums.size(); 4 std::sort(nums.begin(), nums.end()); 5 int closest_sum = nums[0] + nums[1] + nums[2]; 6 7 for(int i = 0; i \u0026lt; length - 2; i++) 8 { 9 int left = i + 1, right = length - 1; 10 while(left \u0026lt; right) 11 { 12 int current_sum = nums[i] + nums[left] + nums[right]; 13 if(current_sum == target) return current_sum; 14 if(std::abs(current_sum - target) \u0026lt; std::abs(closest_sum - target)) 15 closest_sum = current_sum; 16 if(current_sum \u0026lt; target) 17 left++; 18 else 19 right--; 20 } 21 } 22 return closest_sum; 23 } 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/double-pointers/medium/3sum-closest/","summary":"最接近的三数之和的解题思路与 C++ 实现。","title":"最接近的三数之和"},{"content":"题目：电话号码的字母组合\n使用到的知识点是 哈希表 和 回溯算法\n因为未知输入字符串的长度，所以不能手动的定义for循环去暴力求解\n联想到树的结构，从一个分支一直走到底，再返回上一个最近的分支；与此题类似，以前一个字符串为基点，向后遍历每一个出现的字符，组成新的字符串。\n难点在于如何回溯：这里使用到的是递归函数\n根据传入的index确定当前数字，然后再根据数字在map中得到对应的字符，以每一个字符为起点，进行函数的递归调用，直到index与输入字符串长度相同\nletterCombinations（）\n1class Solution 2{ 3public: 4 void backTrack(const string\u0026amp; digits, int index, std::unordered_map\u0026lt;char, string\u0026gt;\u0026amp; phoneMap, string\u0026amp; combination, std::vector\u0026lt;string\u0026gt;\u0026amp; combinations) 5 { 6 if(index == digits.size()) 7 combinations.push_back(combination); 8 else 9 { 10 char num = digits[index]; 11 const string\u0026amp; letters = phoneMap[num]; 12 for(const char\u0026amp; letter : letters) 13 { 14 combination.push_back(letter); 15 backTrack(digits, index + 1, phoneMap, combination, combinations); 16 combination.pop_back(); 17 } 18 } 19 } 20 21 vector\u0026lt;string\u0026gt; letterCombinations(string digits) 22 { 23 if(digits.empty()) return {}; 24 std::unordered_map\u0026lt;char, string\u0026gt; phoneMap 25 { 26 {\u0026#39;2\u0026#39;, \u0026#34;abc\u0026#34;}, 27 {\u0026#39;3\u0026#39;, \u0026#34;def\u0026#34;}, 28 {\u0026#39;4\u0026#39;, \u0026#34;ghi\u0026#34;}, 29 {\u0026#39;5\u0026#39;, \u0026#34;jkl\u0026#34;}, 30 {\u0026#39;6\u0026#39;, \u0026#34;mno\u0026#34;}, 31 {\u0026#39;7\u0026#39;, \u0026#34;pqrs\u0026#34;}, 32 {\u0026#39;8\u0026#39;, \u0026#34;tuv\u0026#34;}, 33 {\u0026#39;9\u0026#39;, \u0026#34;wxyz\u0026#34;} 34 }; 35 std::vector\u0026lt;string\u0026gt; combinations; 36 string combination; 37 backTrack(digits, 0, phoneMap, combination, combinations); 38 return combinations; 39 } 40}; 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/letter-combinations-of-a-phone-number/","summary":"电话号码的字母组合的解题思路与 C++ 实现。","title":"电话号码的字母组合"},{"content":"题目：删除链表的倒数第 N 个结点\n使用的是双指针\n定义左指针left和右指针right，初始都先指向头结点的前一个结点（哨兵节点），然后先将right指针向后移动n的距离，这样当right指针遍历到尾节点时，left指针恰好处于right指针后n个位置，也就是倒数第n个节点。\n到达目标位置之后，先将需要删除的节点保存在一个临时变量中，避免内存泄漏，再将上一个节点的指向改为当前节点的下一个节点，最后delete当前节点，并返回头节点\n1ListNode* removeNthFromEnd(ListNode* head, int n) 2 { 3 ListNode dummyNode(0, head); 4 ListNode* left = \u0026amp;dummyNode; 5 ListNode* right = \u0026amp;dummyNode; 6 for(int i = 0; i \u0026lt; n; i++) 7 { 8 right = right-\u0026gt;next; 9 } 10 11 while(right-\u0026gt;next != nullptr) 12 { 13 right = right-\u0026gt;next; 14 left = left-\u0026gt;next; 15 } 16 auto temp = left-\u0026gt;next; 17 left-\u0026gt;next = left-\u0026gt;next-\u0026gt;next; 18 delete temp; 19 return dummyNode.next; 20 } ","permalink":"https://jiat-t.github.io/leetcode/list/medium/remove-nth-node-from-end-of-list/","summary":"删除链表的倒数第 N 个结点的解题思路与 C++ 实现。","title":"删除链表的倒数第 N 个结点"},{"content":"题目：有效的括号\n使用到的知识点是栈\n利用栈的FILO的特性，可以很好的对相应的括号进行匹配\n比如\u0026quot;{ [ ( ) ] } \u0026quot;\n第一次先压入{，接着判断下一个括号的类型，如果为左括号，则继续压入；如果为相匹配的右括号，则弹出；如果为不匹配的右括号，则直接返回false\n最后如果整个栈都为空，就意味着所有的括号都得到了匹配，那么就返回true\n注意：在一开始的时候，要判断一下特殊情况\u0026mdash;\u0026mdash;\n元素个数为奇数，就意味着总会有一个括号无法被匹配，直接返回false\n1bool isValid(string s) 2 { 3 std::vector\u0026lt;char\u0026gt; stack; 4 if(s.size() % 2 == 1) return false; 5 for(int n = 0; n \u0026lt; s.size(); n++) 6 { 7 if(s[n] == \u0026#39;(\u0026#39; || s[n] == \u0026#39;[\u0026#39; || s[n] == \u0026#39;{\u0026#39;) stack.push_back(s[n]); 8 else 9 { 10 if (stack.empty()) return false; 11 if(s[n] == \u0026#39;)\u0026#39; \u0026amp;\u0026amp; stack.back() == \u0026#39;(\u0026#39;) stack.pop_back(); 12 else if(s[n] == \u0026#39;]\u0026#39; \u0026amp;\u0026amp; stack.back() == \u0026#39;[\u0026#39;) stack.pop_back(); 13 else if(s[n] == \u0026#39;}\u0026#39; \u0026amp;\u0026amp; stack.back() == \u0026#39;{\u0026#39;) stack.pop_back(); 14 else return false; 15 } 16 } 17 if(stack.empty()) return true; 18 return false; 19 } ","permalink":"https://jiat-t.github.io/leetcode/stack/easy/valid-parentheses/","summary":"有效的括号的解题思路与 C++ 实现。","title":"有效的括号"},{"content":"题目：合并两个有序链表\n使用的是递归的方法\n首先判断是否有链表为空，如果是，则直接返回另一个链表\n之后对list1和list2的值进行判断，更小者的下一个节点将调用原函数继续递归（比如情况一，list1的值更小，就让list1的next节点继续调用函数，将list1剩下的节点继续与list2比较）\n1ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) 2 { 3 if(list1 == nullptr) return list2; 4 if(list2 == nullptr) return list1; 5 if(list1-\u0026gt;val \u0026lt; list2-\u0026gt;val) 6 { 7 list1-\u0026gt;next = mergeTwoLists(list1-\u0026gt;next, list2); 8 return list1; 9 } 10 else 11 { 12 list2-\u0026gt;next = mergeTwoLists(list1, list2-\u0026gt;next); 13 return list2; 14 } 15 } ","permalink":"https://jiat-t.github.io/leetcode/list/easy/merge-two-sorted-lists/","summary":"合并两个有序链表的解题思路与 C++ 实现。","title":"合并两个有序链表"},{"content":"题目：括号生成\n使用到的是回溯算法\n对于输入的数字n，可以知道的是最终的元素为2n个括号，其中左括号和右括号都为n个\n先经过一次分析得到规律：以n=2为例，每个元素分别有2个左右括号，第一个空必须是\u0026rsquo; （ ’，此时left的数量加一；而第二个空则有两种可能，当为‘（’时，left的数量已经达到了2，那么之后两个空就只能是‘）’；当为‘）’时，此时left = right，第三个空就只能是‘（’，从而得出第四个空是‘）’\n总结规律，可以得出：\n1.当left \u0026lt; n时，应该添加‘（’\n2.当right \u0026lt; left时，应当有‘）’与其配对，故添加‘）’\n在此条件下进行递归，最后当left+right与n相同时终止最后一层递归，返回上一层的枝，继续下一个子节点的递归\ngenerateParenthesis()\n1class Solution 2{ 3public: 4 vector\u0026lt;string\u0026gt; generateParenthesis(int n) 5 { 6 std::vector\u0026lt;string\u0026gt; combinations; 7 string combination; 8 backTrace(0, 0, combination, combinations, n); 9 return combinations; 10 } 11 12private : 13 void backTrace(int left, int right, string\u0026amp; combination, std::vector\u0026lt;string\u0026gt;\u0026amp; combinations, int n) 14 { 15 if(left + right == 2 * n) 16 { 17 combinations.push_back(combination); 18 return; 19 } 20 else 21 { 22 if(left \u0026lt; n) 23 { 24 combination.push_back(\u0026#39;(\u0026#39;); 25 backTrace(left + 1, right, combination, combinations, n); 26 combination.pop_back(); 27 } 28 if(right \u0026lt; left) 29 { 30 combination.push_back(\u0026#39;)\u0026#39;); 31 backTrace(left, right + 1, combination, combinations, n); 32 combination.pop_back(); 33 } 34 } 35 } 36}; 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/generate-parentheses/","summary":"括号生成的解题思路与 C++ 实现。","title":"括号生成"},{"content":"题目：合并 K 个升序链表\n参考：https://leetcode.cn/problems/merge-k-sorted-lists/solutions/2384305/liang-chong-fang-fa-zui-xiao-dui-fen-zhi-zbzx/?envType=problem-list-v2\u0026amp;envId=91oI3WTD\n首先明确思路：对于第一个插入的节点，一定是某一个链表的头节点，而第二个节点，不是当前节点的下一个节点，就是其他链表的头节点\u0026hellip;此时需要有一个数据结构，能够找到并去除最小节点，同时还要能插入新节点——这里使用的是“最小堆”\n首先定义初始状态——将三个头节点存入堆中，然后找出最小节点作为头节点；接着判断被删去的节点是否存在下一个节点，如果有，就将其存入堆中，之后进行进行相同的判断，直到堆为空\nmergeKLists（）\n1ListNode* mergeKLists(vector\u0026lt;ListNode*\u0026gt;\u0026amp; lists) 2{ 3 auto cmp = [](ListNode* a, ListNode* b) 4 { 5 return a-\u0026gt;val \u0026gt; b -\u0026gt; val; 6 }; 7 std::priority_queue\u0026lt;ListNode*, std::vector\u0026lt;ListNode*\u0026gt;, decltype(cmp)\u0026gt; less_heap; 8 9 for(auto head : lists) 10 { 11 if(head != nullptr) 12 less_heap.push(head); 13 } 14 15 ListNode dummy(0); 16 ListNode* head = \u0026amp;dummy; 17 while(!less_heap.empty()) 18 { 19 auto node = less_heap.top(); 20 less_heap.pop(); 21 if(node-\u0026gt;next != nullptr) 22 { 23 less_heap.push(node-\u0026gt;next); 24 } 25 head-\u0026gt;next = node; 26 head = head-\u0026gt;next; 27 } 28 return dummy.next; 29} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/list/hard/merge-k-sorted-lists/","summary":"合并 K 个升序链表的解题思路与 C++ 实现。","title":"合并 K 个升序链表"},{"content":"题目：两两交换链表中的节点\n使用的是迭代\n就像之前写过的大部分链表的题目一样，要创建一个哨兵节点（前置节点）\n首先哨兵节点指向第一个节点，之后进入循环，前置节点指向第二个节点，第一个节点指向第三个节点，最后让第二个节点指向原来的第一个节点，将前置节点指向此时的第二个节点，进入下一次循环\n1ListNode* swapPairs(ListNode* head) 2 { 3 if( !head || !head-\u0026gt;next ) return head; 4 ListNode header1(0, head); 5 ListNode* prev = \u0026amp;header1; 6 7 while(prev-\u0026gt;next \u0026amp;\u0026amp; prev-\u0026gt;next-\u0026gt;next) 8 { 9 auto node1 = prev-\u0026gt;next; 10 auto node2 = prev-\u0026gt;next-\u0026gt;next; 11 12 prev-\u0026gt;next = node2; 13 node1-\u0026gt;next = node2-\u0026gt;next; 14 node2-\u0026gt;next = node1; 15 16 prev = node1; 17 } 18 19 return header1.next; 20 } 补充解法：递归交换相邻节点\n1ListNode* swapPairs(ListNode* head) 2{ 3 if(head == nullptr || head-\u0026gt;next == nullptr) return head; 4 5 ListNode* newHead = head-\u0026gt;next; 6 head-\u0026gt;next = swapPairs(newHead-\u0026gt;next); 7 newHead-\u0026gt;next = head; 8 return newHead; 9} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/list/medium/swap-nodes-in-pairs/","summary":"两两交换链表中的节点的解题思路与 C++ 实现。","title":"两两交换链表中的节点"},{"content":"题目：K 个一组翻转链表\n1ListNode* reverseKGroup(ListNode* head, int k) 2{ 3 // 统计节点个数 4 int n = 0; 5 for(ListNode* node = head; node; node = node-\u0026gt;next) 6 { 7 n++; 8 } 9 10 ListNode dummy(0, head); 11 ListNode* trailer = \u0026amp;dummy; 12 ListNode* curr = head; 13 ListNode* prev = nullptr; 14 15 //外层循环：确保每次只翻转 k 个节点 16 for(; k \u0026lt;= n; n -= k) 17 { 18 // 内层循环：执行翻转的操作 19 for(int i = 0; i \u0026lt; k; i++) 20 { 21 ListNode* temp = curr-\u0026gt;next; 22 curr-\u0026gt;next = prev; 23 prev = curr; 24 curr = temp; 25 } 26 27 // 将翻转后的节点组与其他节点进行连接 28 /* trailer-\u0026gt;next 原本指向的是翻转链表的第一个节点 29 经过翻转之后，这个节点变成了最后一个节点 */ 30 ListNode* nxt = trailer-\u0026gt;next; 31 trailer-\u0026gt;next = prev; 32 nxt-\u0026gt;next = curr; 33 // 将 trailer 移动到下一组的前一个节点 34 trailer = nxt; 35 } 36 return dummy.next; 37} ","permalink":"https://jiat-t.github.io/leetcode/list/hard/reverse-nodes-in-k-group/","summary":"K 个一组翻转链表的解题思路与 C++ 实现。","title":"K 个一组翻转链表"},{"content":"题目：删除有序数组中的重复项\n笔记 1 使用的是双指针\n这里如果使用最原始的方法：如果当前元素与上一个相同则删除(erase），会导致O(n^2)的时间复杂度——————erase（）会将删除元素后面的元素向前移动\n而这里，我们定义了两个指针，一个慢指针（k），一个快指针（i）；将两个指针对应的值进行比较，如果不同，就将慢指针后移，同时将慢指针所指元素覆盖为当前元素\n在这个方法中，我们并没有真正执行“删除”操作，而是对数组进行了重新排列，将重复的元素放到数组末尾，因为最终的返回值是有效的数字个数，因此末尾的区域也就会变成垃圾数据，不需要再去关心它们\nremoveDuplicates（） 同一份实现已收录于第 26 题解法。\n来源：语雀原笔记\n笔记 2 使用的是双指针\n这里如果使用最原始的方法：如果当前元素与上一个相同则删除(erase），会导致O(n^2)的时间复杂度——————erase（）会将删除元素后面的元素向前移动\n而这里，我们定义了两个指针，一个慢指针（k），一个快指针（i）；将两个指针对应的值进行比较，如果不同，就将慢指针后移，同时将慢指针所指元素覆盖为当前元素\n在这个方法中，我们并没有真正执行“删除”操作，而是对数组进行了重新排列，将重复的元素放到数组末尾，因为最终的返回值是有效的数字个数，因此末尾的区域也就会变成垃圾数据，不需要再去关心它们\n1int removeDuplicates(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2 { 3 if(nums.empty()) return 0; 4 5 int k = 0; 6 for(int i = 1; i \u0026lt; nums.size(); i++) 7 { 8 if(nums[i] != nums[k]) 9 { 10 k++; 11 nums[k] = nums[i]; 12 } 13 } 14 return k + 1; 15 } ","permalink":"https://jiat-t.github.io/leetcode/double-pointers/easy/remove-duplicates-from-sorted-array/","summary":"删除有序数组中的重复项的解题思路与 C++ 实现。","title":"删除有序数组中的重复项"},{"content":"题目：移除元素\n解法一：\n先对整个数组进行排序，将相同元素堆到一起然后通过两次遍历找到重复元素的起始、终止下标最后一键清除 removeElement（）\n1int removeElement(vector\u0026lt;int\u0026gt;\u0026amp; nums, int val) 2{ 3 std::sort(nums.begin(), nums.end()); 4 int start = 0; 5 while(start \u0026lt; nums.size() \u0026amp;\u0026amp; nums[start] != val) 6 { 7 start++; 8 } 9 if(start == nums.size()) return nums.size(); 10 int end = start; 11 while(end \u0026lt; nums.size() \u0026amp;\u0026amp; nums[end] == val) 12 { 13 end++; 14 } 15 16 nums.erase(nums.begin() + start, nums.begin() + end); 17 return nums.size(); 18} 解法二： 使用到的是快慢指针\n快指针负责遍历整个数组，慢指针负责指向下一个“不等于 val”的元素的位置\n如果 fast 指向的不是 val，就将这个元素放到前面去，最后 slow 前面的元素代表的就全是“不等于 val”的数字\nremoveElement（）\n1int removeElement(vector\u0026lt;int\u0026gt;\u0026amp; nums, int val) 2{ 3 int slow = 0, fast = 0; 4 for(; fast \u0026lt; nums.size(); fast++) 5 { 6 if(nums[fast] != val) 7 { 8 nums[slow] = nums[fast]; 9 slow++; 10 } 11 } 12 return slow; 13} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/double-pointers/easy/remove-element/","summary":"移除元素的解题思路与 C++ 实现。","title":"移除元素"},{"content":"题目：下一个排列\n思路：找到下一个排序，意思就是将当前数组视作一串数字，通过对元素的重新组合，找到第一个大于当前数字的数字\n具体实现： 首先从后往前找到第一个逆序的数字，保存它的下标——如：123546718，这里会保存数字“4”的下标，也就是 4，之后再在 46718 这个范围内从后往前寻找第一个小于 5 的数字，并将其进行交换（123146758）；最后将46758进行降序·排序即可得到下一个排序\n1void nextPermutation(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int sz = nums.size(); 4 if(sz \u0026lt;= 1) return; 5 int i = sz - 1; 6 while(0 \u0026lt; i \u0026amp;\u0026amp; nums[i] \u0026lt;= nums[i - 1]) 7 { 8 i--; 9 } 10 11 // 如果 i == 0，说明整个序列都是降序的（如 54321），已经是最大排列 12 if(0 \u0026lt; i) 13 { 14 int j = sz - 1; 15 while(nums[j] \u0026lt;= nums[i - 1]) 16 { 17 j--; 18 } 19 20 std::swap(nums[i - 1], nums[j]); 21 } 22 std::sort(nums.begin() + i, nums.end()); 23} ","permalink":"https://jiat-t.github.io/leetcode/technique/medium/next-permutation/","summary":"下一个排列的解题思路与 C++ 实现。","title":"下一个排列"},{"content":"题目：搜索旋转排序数组\n一开始的想法是：先遍历一遍旋转后的数组，分别找到两个各自单调的子数组，之后对这两个数组分别进行二分查找；但这会导致线性的时间复杂度，而不是题目要求的logn\n而下面这种方法就是不通过遍历，直接在二分查找中对有序的子数组进行判断\n具体实现：\n仍然是先定义左右边界（左闭右开），之后进入循环\n当mid对应的值恰好与target相等时，则mid就是我们要找的下标\n若是不等，则根据当前中点值与最右值得比较结果，对左数组与右数组进行选择\n之后再在这两个子数组中进行二分查找\n1int search(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 2{ 3 int left = 0, right = nums.size() - 1; 4 while(left \u0026lt;= right) 5 { 6 int mid = left + (right - left) / 2; 7 if(nums[mid] == target) 8 return mid; 9 else if(nums[left] \u0026lt;= nums[mid]) 10 { 11 // 进入左数组 12 if(nums[left] \u0026lt;= target \u0026amp;\u0026amp; target \u0026lt; nums[mid]) 13 right = mid; 14 else 15 left = mid + 1; 16 } 17 else 18 { 19 //进入右数组 20 if(nums[mid] \u0026lt; target \u0026amp;\u0026amp; target \u0026lt;= nums[right]) 21 left = mid + 1; 22 else 23 right = mid; 24 } 25 } 26 return -1; 27} ","permalink":"https://jiat-t.github.io/leetcode/binary-search/medium/search-in-rotated-sorted-array/","summary":"搜索旋转排序数组的解题思路与 C++ 实现。","title":"搜索旋转排序数组"},{"content":"题目：在排序数组中查找元素的第一个和最后一个位置\nlowerBound函数：返回第一个 \u0026gt;= target的数的下标\n实现原理：内部是一个while循环，当循环结束时，应当恰好有 left = right 成立，此时right代表\t的就是“第一个 \u0026gt;= target的数的下标”————\n- **左阵营 (**`**left**`**)**：在这个索引左边的数，我们确定**全部**都 \u0026lt; target - **右阵营 (**`**right**`**)**：在这个索引（包含自身）右边的数，我们确定**全部**都 \u0026gt;= target 又因为target的第一个数恰好满足这个条件，使用lowerBound返回的实际上是target的右边界\n之后在主函数中如法炮制，通过求得target+1的首位，再减去1，所得到的就是target的右边界\n1vector\u0026lt;int\u0026gt; searchRange(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 2{ 3 int start = lowerBound(nums, target); 4 if(start == nums.size() || nums[start] != target) 5 return {-1, -1}; 6 int end = lowerBound(nums, target + 1) - 1; 7 return {start, end}; 8} 9 10// 返回第一个 \u0026gt;= target的数的下标 11int lowerBound(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 12{ 13 int left = 0, right = nums.size(); 14 while(left \u0026lt; right) 15 { 16 int mid = left + (right - left) / 2; 17 18 if(target \u0026lt;= nums[mid]) 19 right = mid; 20 else 21 left = mid + 1; 22 } 23 return right; 24} 补充解法：标准库 lower_bound\n1vector\u0026lt;int\u0026gt; searchRange(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 2{ 3 if(nums.empty()) return{-1,-1}; 4 5 int start = lower_bound(nums.begin(), nums.end(), target) - nums.begin(); 6 7 if(start == nums.size() || nums[start] != target) 8 return {-1, -1}; 9 10 // 因为这里得到的是第一个 target + 1 的元素的下标 11 // 所以需要减一得到的才是 target 最后一个元素的下标 12 int end = lower_bound(nums.begin(), nums.end(), target + 1) - nums.begin() - 1; 13 return {start, end}; 14} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/binary-search/medium/find-first-and-last-position-of-element-in-sorted-array/","summary":"在排序数组中查找元素的第一个和最后一个位置的解题思路与 C++ 实现。","title":"在排序数组中查找元素的第一个和最后一个位置"},{"content":"题目：搜索插入位置\n定义左闭右开区间\n当left = right时，意味着值被找到了\n1int searchInsert(vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) 2{ 3 int left = 0, right = nums.size(); 4 while(left \u0026lt; right) 5 { 6 int mid = left + (right - left) / 2; 7 if(nums[mid] \u0026lt; target) 8 left = mid + 1; 9 else 10 right = mid; 11 } 12 return left; 13} ","permalink":"https://jiat-t.github.io/leetcode/binary-search/easy/leetcode-binary-search-easy/","summary":"搜索插入位置的解题思路与 C++ 实现。","title":"搜索插入位置"},{"content":"题目：组合总和\n核心思路：每次添加元素之后都将target减少对应的值，直到等于或小于零\n具体实现：\n在回溯函数中首先判断target是否已经等于零——代表着当前元素的组合已经满足要求，可以压入res数组中\n否则继续进行遍历与递归，直到出现满足总和要求的数组为止\n在循环中要注意的是，当总和已经超出target时，就不要在把当前元素存入temp中了，而是直接跳过他，从下一个元素继续进行\n1void backTrack(vector\u0026lt;int\u0026gt;\u0026amp; candidates, int target, int index, vector\u0026lt;int\u0026gt;\u0026amp; temp, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res) 2{ 3 if(target == 0) 4 { 5 res.push_back(temp); 6 return; 7 } 8 9 for(int i = index; i \u0026lt; candidates.size(); i++) 10 { 11 if(target - candidates[i] \u0026lt; 0) continue; 12 temp.push_back(candidates[i]); 13 backTrack(candidates, target - candidates[i], i, temp, res); 14 temp.pop_back(); 15 } 16} 17vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; combinationSum(vector\u0026lt;int\u0026gt;\u0026amp; candidates, int target) 18{ 19 vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; 20 vector\u0026lt;int\u0026gt; temp; 21 res.reserve(150); 22 backTrack(candidates, target, 0, temp, res); 23 return res; 24} ","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/combination-sum/","summary":"组合总和的解题思路与 C++ 实现。","title":"组合总和"},{"content":"题目：跳跃游戏 II\n这里直接引用一下别人的讲解：\n//把该问题比喻为入职，数组下标是公司级别(入职门槛)，对应的值是公司级别之上的级别成长空间。你想用最少的跳槽次数入职最高级的公司\n//数组[2,3,1,2,4,2,3]\n//下标 0 1 2 3 4 5 6\n//最开始你没有工作，你的水平是2级，可以在2级及以下的公司里随便挑，此时候选公司有下标1和2\n//注意：如果你想进入更高级别的公司，应该选择能够帮助你提升级别最多的公司。例如，公司1能够将你的级别提升到1+3=4级，而公司2只能提升到2+1=3级。显然，你应该选择公司1，然后升到4级。接下来，你可以跳槽到4级及以下的公司，每次都选择能够帮助你提升最多级别的公司，如此循环……直到水平级别足够入职梦中情司\n//只有每次跳槽都选择能够帮助你升级最多的公司，才能以最少的跳槽次数到达最高级别的公司\n1int jump(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int res = 0; 4 int curr = 0, max_pos = 0; 5 6 for(int i = 0; i \u0026lt; nums.size() - 1; i++) 7 { 8 max_pos = std::max(max_pos, i + nums[i]); 9 if(i == curr) 10 { 11 curr = max_pos; 12 res++; 13 } 14 } 15 return res; 16} ","permalink":"https://jiat-t.github.io/leetcode/greedy-algorithm/medium/jump-game-ii/","summary":"跳跃游戏 II的解题思路与 C++ 实现。","title":"跳跃游戏 II"},{"content":"题目：全排列\n核心在于“交换”的步骤\n首先从第一个元素开始，既然首元素已经确定了，之后就是递归地对后面的元素进行全排列\n本质上就是让所有元素都当一次第一个元素，再让此时的第一个元素之后的所有元素当一次第二个元素\u0026hellip;..以此类推，从前往后每个位置分别有n、n-1、n-2个种选择，也就是n！种排列方式\n1vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; permute(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; 4 res.reserve(factorial(nums.size())); 5 6 backTrack(nums, res, 0); 7 return res; 8} 9 10void backTrack(vector\u0026lt;int\u0026gt;\u0026amp; nums, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res, int first) 11{ 12 if(first == nums.size()) 13 res.push_back(nums); 14 15 for(int i = first; i \u0026lt; nums.size(); i++) 16 { 17 std::swap(nums[first], nums[i]); 18 backTrack(nums, res, first + 1); 19 std::swap(nums[first], nums[i]); 20 } 21} 22 23size_t factorial(size_t sz) 24{ 25 size_t res; 26 for(int i = 0; i \u0026lt; sz; i++) res *= i; 27 return res; 28} 补充解法：used 数组回溯\n1class Solution 2{ 3public: 4 std::vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; 5 std::vector\u0026lt;int\u0026gt; path; 6 void backTrack(vector\u0026lt;int\u0026gt;\u0026amp; nums, vector\u0026lt;bool\u0026gt; used) 7 { 8 // 终止条件 9 if(path.size() == nums.size()) 10 { 11 res.push_back(path); 12 return; 13 } 14 15 for(int i = 0; i \u0026lt; nums.size(); i++) 16 { 17 // 如果当前元素已经被使用过了 18 // 就直接跳过它 19 if(used[i]) 20 continue; 21 22 // 将当前元素标记为“已使用” 23 used[i] = true; 24 path.push_back(nums[i]); 25 // 对下一个元素进行操作 26 backTrack(nums, used); 27 // 撤销所有操作 28 path.pop_back(); 29 used[i] = false; 30 } 31 } 32 33 vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; permute(vector\u0026lt;int\u0026gt;\u0026amp; nums) 34 { 35 // 用来标记已使用的元素 36 std::vector\u0026lt;bool\u0026gt; used(nums.size(), false); 37 backTrack(nums, used); 38 return res; 39 } 40}; 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/permutations/","summary":"全排列的解题思路与 C++ 实现。","title":"全排列"},{"content":"题目：旋转图像\n以矩阵的四个对角点为例（左上A，右上B，右下C，左下D），从A开始，顺序是D-\u0026gt;A, A-\u0026gt;B, B-\u0026gt;C, C-\u0026gt;D\n不过这会出现一个状况，就是当 D 执行旋转之后，A 的值就已经被覆盖了，所以需要额外使用一个临时变量用于存储 A 的值，在最后的时候再覆盖到原来 B 的位置\n接下来就是需要得到元素的旋转公式\n如图，可以总结出元素的旋转公式是：\nmatrix [ i ][ j ] 原索引位置→matrix [ j ][ n −1−i ]→旋转后索引位置\n之后就是按照一开始的思路进行赋值就行\n唯一要注意的是循环的边界条件：0 \u0026lt;= i \u0026lt; n / 2 , 0 \u0026lt;= j \u0026lt; (n + 1) / 2\n至于为什么是这样的范围：因为对于矩阵中的单个元素来说，对他进行一次旋转就相当于挪动了四个元素的位置，所以我们只需要遍历矩阵的四分之一就行了，这也就是 i，j 右边界不为n的原因\n而 j 的右边界之所以是 (n + 1) / 2，是因为要考虑奇数矩阵与偶数矩阵（前者有中心元素，后者没有）\n因为这里的 “ /2 ”是整数除法，所以可以确保阶数为奇数时，中心元素不进行处理\n1 2void rotate(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; matrix) 3 4{ 5 6 int n = matrix.size(); 7 8 for(int i = 0; i \u0026lt; n / 2; i++) 9 10 { 11 12 for(int j = 0; j \u0026lt; (n + 1) / 2; j++) 13 14 { 15 16 int temp = matrix[i][j]; 17 18 matrix[i][j] = matrix[n - 1 - j][i]; 19 20 matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; 21 22 matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; 23 24 matrix[j][n - 1 - i] = temp; 25 26 } 27 28 } 29 30} ","permalink":"https://jiat-t.github.io/leetcode/matrix/medium/rotate-image/","summary":"旋转图像的解题思路与 C++ 实现。","title":"旋转图像"},{"content":"题目：字母异位词分组\n实现思路：记录每个字符串中单个字母的出现频率，并以字符串形式存储起来（如”0110200000\u0026hellip;.\u0026quot;)，可以提前将字符串的空间预留26位（对应26个字母），然后将这个字符串作为哈希表的key，用来查找表中存储着异位词的vector值\n1vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; groupAnagrams(vector\u0026lt;string\u0026gt;\u0026amp; strs) 2{ 3 std::vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; res; 4 std::unordered_map\u0026lt;string, std::vector\u0026lt;string\u0026gt;\u0026gt; um_pair; 5 for(auto\u0026amp; str : strs) 6 { 7 string count(26, 0); 8 for(auto\u0026amp; c : str) 9 { 10 count[c - \u0026#39;a\u0026#39;] += 1; 11 } 12 um_pair[count].push_back(str); 13 } 14 for(auto\u0026amp; pair : um_pair) 15 { 16 res.push_back(pair.second); 17 } 18 return res; 19} ","permalink":"https://jiat-t.github.io/leetcode/hash/medium/group-anagrams/","summary":"字母异位词分组的解题思路与 C++ 实现。","title":"字母异位词分组"},{"content":"题目：最大子数组和\n核心判断条件：\n如果前一段子序列之和与当前元素相加之后，还没有当前元素本身大，那么就直接从当前元素开始重新计算\n具体实现：\n首先定义两个和，一个用于记录目前为止的最大和（max_val），一个用于在循环中进行判断（curr_val）\n之后进入循环，通过上述判断条件对当前最大和进行更新，再用当前最大和与之前保持的最大和进行比较\nmaxSubArray（）\n1int maxSubArray(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int curr_val = nums[0]; 4 int max_val = nums[0]; 5 6 for(int i = 1; i \u0026lt; nums.size(); i++) 7 { 8 if(curr_val + nums[i] \u0026lt; nums[i]) 9 curr_val = nums[i]; 10 else 11 curr_val = curr_val + nums[i]; 12 13 if(curr_val \u0026gt; max_val) 14 max_val = curr_val; 15 } 16 return max_val; 17} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/array/medium/maximum-subarray/","summary":"最大子数组和的解题思路与 C++ 实现。","title":"最大子数组和"},{"content":"题目：螺旋矩阵\n对res数组赋值的具体流程我放在注释里了\n其实思路倒还是不难想到，就是代码实现可能需要动下脑子\n这里是定义了原矩阵的上、下、左、右边界，而螺旋移动的情况有四种：\n1.从左向右（在顶部）：相当于消去了一行，因此上边界需要收缩一行\n2.从上到下（在右端）：相当于消去了一列，因此右边界收缩一列\n3.从右到左（在底部）：同理，下边界向上收缩一行\n4.从下到上（在左端）：同理，左边界向右收缩一列\n具体的方向的控制是通过direction对4求模实现的，数字0、1、2、3对应的方向我也写在注释里了\n1 2vector\u0026lt;int\u0026gt; spiralOrder(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; matrix) 3 4{ 5 6 // 设一个 n * m 的矩阵 7 8 // （第 0 行 -\u0026gt; 第 m - 1 列 -\u0026gt; 第 n - 1 行 -\u0026gt; 第 0 列） -\u0026gt; 9 10 // （第 1 行 -\u0026gt; 第 m - 2 列 -\u0026gt; 第 n - 2 行 -\u0026gt; 第 1 列） -\u0026gt; 11 12 // （第 2 行 -\u0026gt; 第 m - 3 列 -\u0026gt; 第 n - 3 行 -\u0026gt; 第 2 列） -\u0026gt; ..... 13 14 15 16 int rows = matrix.size(); // n 17 18 int cols = matrix[0].size(); // m 19 20 std::vector\u0026lt;int\u0026gt; res; 21 22 res.reserve(rows * cols); 23 24 25 26 int top = 0; 27 28 int left = 0; 29 30 int bottom = rows - 1; 31 32 int right = cols - 1; 33 34 35 36 int direction = 0; // 0:右, 1:下, 2:左, 3:上 37 38 39 40 // 只要围墙没有互相穿透，就继续走 41 42 while(top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right) 43 44 { 45 46 switch(direction % 4) 47 48 { 49 50 case 0: 51 52 for(int i = left; i \u0026lt;= right; i++) 53 54 res.push_back(matrix[top][i]); 55 56 top++; 57 58 break; 59 60 61 62 case 1: 63 64 for(int i = top; i \u0026lt;= bottom; i++) 65 66 res.push_back(matrix[i][right]); 67 68 right--; 69 70 break; 71 72 73 74 case 2: 75 76 for(int i = right; left \u0026lt;= i; i--) 77 78 res.push_back(matrix[bottom][i]); 79 80 bottom--; 81 82 break; 83 84 85 86 case 3: 87 88 for(int i = bottom; top \u0026lt;= i; i--) 89 90 res.push_back(matrix[i][left]); 91 92 left++; 93 94 break; 95 96 } 97 98 direction++; 99 100 } 101 102 return res; 103 104} ","permalink":"https://jiat-t.github.io/leetcode/matrix/medium/spiral-matrix/","summary":"螺旋矩阵的解题思路与 C++ 实现。","title":"螺旋矩阵"},{"content":"题目：跳跃游戏\n思路：\n从第一个元素开始进行遍历，并记录截止当前元素可以到达的最大位置（far）\n如果当前位置的下标（i）比最远位置还要大，就说明 i 是无法到达的，也就更不可能到达终点\n而如果 i 在 far 的范围之内，就说明 i 是可以到达的，此时就需要根据 i 对应的值来更新 far 的位置\n当整个数组都被遍历完成，就说明最后一个元素是位于 far 之内的，也就代表着终点可以到达\n1bool canJump(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int far = 0; 4 for(int i = 0; i \u0026lt; nums.size(); i++) 5 { 6 if(far \u0026lt; i) return false; 7 else 8 far = std::max(far, i + nums[i]); 9 } 10 return true; 11} ","permalink":"https://jiat-t.github.io/leetcode/greedy-algorithm/medium/jump-game/","summary":"跳跃游戏的解题思路与 C++ 实现。","title":"跳跃游戏"},{"content":"题目：合并区间\n思路：\n难点在于“如何对重叠区间进行合并”，这里选用的判断条件是\u0026mdash;如果当前区间左边界小于结果数组尾部区间的右边界，则选取两者中更大的右边界进行合并\n具体实现：\n首先以左边界为基准对原数组进行了排序，这样就能从左往右更好地判断是否重叠\n这里使用到了lambda表达式，它的语法是：\n// [捕获列表](参数列表) -\u0026gt; 返回值类型 { 函数体 }\nauto my_func = [ ](int a, int b) { return a + b; };\n但实际上，返回值类型通常可以省略，让编译器自己去猜。所以最常用的长这样：\n[] (int a, int b) { return a + b; }\n至于这里为什么是直接在res中进行修改，这是因为可以避免多次push_back的开销\nmerge（）\n1vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; merge(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; intervals) 2{ 3 if(intervals.empty()) return {}; 4 std::sort(intervals.begin(), intervals.end(), [](const vector\u0026lt;int\u0026gt;\u0026amp; a, const vector\u0026lt;int\u0026gt;\u0026amp; b){return a[0] \u0026lt; b[0];}); 5 6 vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res{intervals[0]}; 7 8 for(int i = 1; i \u0026lt; intervals.size(); i++) 9 { 10 // 当前元素左边界小于等于上一个区间右边界 --\u0026gt; 发生重合！ 11 if(intervals[i][0] \u0026lt;= res.back()[1]) 12 // 取更大的有边界合并 13 res.back()[1] = std::max(res.back()[1], intervals[i][1]); 14 else 15 res.push_back(intervals[i]); 16 } 17 return res; 18} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/array/medium/merge-intervals/","summary":"合并区间的解题思路与 C++ 实现。","title":"合并区间"},{"content":"题目：不同路径\n解法一：\n定义状态：dp[ i ][ j ] 指的是”走到点（i，j）的不同路径数量\n状态转移：dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp[ i ][ j - 1]，即“要想走到点(i, j)，只有两个选择——从（i-1，j）向下走，或者从（i，j-1）向右走\n1int uniquePaths(int m, int n) 2{ 3 int dp[m][n]; 4 for(int i = 0; i \u0026lt; m; i++) dp[i][0] = 1; 5 for(int i = 0; i \u0026lt; n; i++) dp[0][i] = 1; 6 for(int i = 1; i \u0026lt; m; i++) 7 { 8 for(int j = 1; j \u0026lt; n; j++) 9 { 10 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; 11 } 12 } 13 return dp[m - 1][n - 1]; 14} 解法二：\n第一个方法的空间复杂度是m*n，然而在每一次的计算中，实际只用到了两个元素，所以可以进行优化\n只需要维护一个一维数组，dp[j]代表着当前元素，使用dp[j]自加dp[j-1]（左边的元素），等价于 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]，因为旧的dp[j]就是当前元素的上面一个元素\n1int uniquePaths(int m, int n) 2{ 3 std::vector\u0026lt;int\u0026gt; dp(n, 1); 4 for(int i = 1; i \u0026lt; m; i++) 5 { 6 for(int j = 1; j \u0026lt; n; j++) 7 { 8 dp[j] += dp[j - 1]; 9 } 10 } 11 return dp[n - 1]; 12} ","permalink":"https://jiat-t.github.io/leetcode/multidimensional-dynamic-programming/medium/unique-paths/","summary":"不同路径的解题思路与 C++ 实现。","title":"不同路径"},{"content":"题目：最小路径和\n相关笔记：不同路径\n和上面那题基本一样，只不过状态转移方程变成了**”dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]“**\n1int minPathSum(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; grid) 2{ 3 int r = grid.size(), c = grid[0].size(); 4 int dp[r][c]; 5 dp[0][0] = grid[0][0]; 6 for(int i = 1; i \u0026lt; r; i++) dp[i][0] = grid[i][0] + dp[i - 1][0]; 7 for(int i = 1; i \u0026lt; c; i++) dp[0][i] = grid[0][i] + dp[0][i - 1]; 8 9 for(int i = 1; i \u0026lt; r; i++) 10 { 11 for(int j = 1; j \u0026lt; c; j++) 12 { 13 dp[i][j] = std::min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; 14 } 15 } 16 return dp[r - 1][c - 1]; 17} 补充解法：一维滚动数组\n1int minPathSum(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; grid) 2 { 3 if(grid.empty()) return 0; 4 int m = grid.size(), n = grid[0].size(); 5 std::vector\u0026lt;int\u0026gt; dp(n, 0); 6 7 // 初始化第一行 8 dp[0] = grid[0][0]; 9 for(int i = 1; i \u0026lt; n; i++) 10 { 11 dp[i] = dp[i - 1] + grid[0][i]; 12 } 13 14 // 填充其余行 15 for(int i = 1; i \u0026lt; m; i++) 16 { 17 // 先更新每行第一个元素 18 dp[0] += grid[i][0]; 19 for(int j = 1; j \u0026lt; n; j++) 20 { 21 dp[j] = std::min(dp[j], dp[j - 1]) + grid[i][j]; 22 } 23 } 24 return dp[n - 1]; 25 } 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/multidimensional-dynamic-programming/medium/minimum-path-sum/","summary":"最小路径和的解题思路与 C++ 实现。","title":"最小路径和"},{"content":"题目：爬楼梯\n简单的斐波那契公式\n要注意的是，这里使用递归会超时，然后最好不要使用数组（因为除了当前值的前两个，再往前的数据更本就用不到，应该使用临时变量，存储在栈上，用完就释放内存）\n1int climbStairs(int n) 2{ 3 if(n \u0026lt;= 2) return n; 4 5 int prev1 = 1, prev2 = 2; 6 int curr = 0; 7 for(int i = 3; i \u0026lt;= n; i++) 8 { 9 curr = prev1 + prev2; 10 prev1 = prev2; 11 prev2 = curr; 12 } 13 return curr; 14} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/easy/climbing-stairs/","summary":"爬楼梯的解题思路与 C++ 实现。","title":"爬楼梯"},{"content":"题目：编辑距离\n和下面一题差不多，需要处理的只有两种情况——字符相同与不相同\n1.相同：此时不需要进行任何处理，直接与上一次的结果保持相同即可（dp[i][j] = dp[i - 1][j - 1]）\n2.不相同：这是问题最核心的地方，此时对于两个字符串中的两个字符，有三种处理方法——插入，删除，替换；其中，\n插入代表“在当前长度下的word1的末尾插入word2的当前字符”，对应代码为 dp[i][j-1] + 1；\n删除代表“删除当前长度下的word1的最后一个字符”，对应代码为 dp[i-1][j] + 1；\n替换代表“将当前长度下的word1的最后一个字符替换为当前长度下的word2的尾字符”对应代码为dp[i - 1][j] + 1\n在执行完三个操作后，会分别产生三个不同规模的子问题，我们需要选取其中规模最小的子问题 : std::min()\n左上 = 替换\n上 = 删除（word1 少一个）\n左 = 插入（word2 少一个）\nj-1 j i-1 左上 上\ni 左 ?\n1int minDistance(string word1, string word2) 2{ 3 int n1 = word1.size(), n2 = word2.size(); 4 std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; dp(n1 + 1, vector\u0026lt;int\u0026gt;(n2 + 1)); 5 for(int i = 0; i \u0026lt;= n1; i++) dp[i][0] = i; 6 for(int j = 0; j \u0026lt;= n2; j++) dp[0][j] = j; 7 8 for(int i = 1; i \u0026lt;= n1; i++) 9 { 10 for(int j = 1; j \u0026lt;= n2; j++) 11 { 12 if(word1[i - 1] == word2[j - 1]) 13 { 14 dp[i][j] = dp[i - 1][j - 1]; 15 } 16 else 17 { 18 dp[i][j] = std::min(dp[i - 1][j], std::min(dp[i - 1][j - 1], dp[i][j - 1])) + 1; 19 } 20 } 21 } 22 return dp[n1][n2]; 23} ","permalink":"https://jiat-t.github.io/leetcode/multidimensional-dynamic-programming/medium/edit-distance/","summary":"编辑距离的解题思路与 C++ 实现。","title":"编辑距离"},{"content":"题目：矩阵置零\n比较暴力的方法就是：额外使用一个m*n的矩阵，接着对原矩阵进行遍历，每次遇到零都将额外的矩阵中对应的行列置为零——时间和空间复杂度都为O(n^2)\n不过我们可以使用更简单的方法\n思路如下：\n将第一行和第一列作为我们判断的标准，如果 martix[i][j] 为零，那么就相应地将 matrix[i][0] 和 matrix[0][j] 置为零。之后再通过一次遍历，如果 matrix[i][0] 和 matrix[0][j] 其中有一者为零，那么 _martix[i][j] _也等于零\n但是这样会导致一个错误，比如说，当第一行全为1，第一列全为0（除了matrix[0][0])时，matrix[0][0]会因为所在列存在零，而被置为零，从而导致第二次遍历时，第一行的元素因为所在行存在零而全被置为0，进而导致整个矩阵全部都为零\n为了解决这个问题，我们需要提前知道第一行和第一列的情况，并单独对他们进行处理；\n- 所以先遍历第一行和第一列，看看他们是否含有零；\n- 之后再处理除了一行一列的其他行列；\n- 最后单独为一行一列赋值\n1 2void setZeroes(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; matrix) 3 4 { 5 6 const int rows = matrix.size(); 7 8 const int cols = matrix[0].size(); 9 10 bool row_has_zero = false, col_has_zero = false; 11 12 13 14 /////////////////////////////////////////////////////////////////////////// 15 16 // 先遍历第一行和第一列 17 18 /////////////////////////////////////////////////////////////////////////// 19 20 for(int i = 0; i \u0026lt; cols; i++) 21 22 { 23 24 if(matrix[0][i] == 0) row_has_zero = true; 25 26 } 27 28 29 30 for(int i = 0; i \u0026lt; rows; i++) 31 32 { 33 34 if(matrix[i][0] == 0) col_has_zero = true; 35 36 } 37 38 39 40 /////////////////////////////////////////////////////////////////////////// 41 42 // 再处理除了一行一列的其他行列（注意是从1开始） 43 44 /////////////////////////////////////////////////////////////////////////// 45 46 for(int i = 1; i \u0026lt; rows; i++) 47 48 { 49 50 for(int j = 1; j \u0026lt; cols; j++) 51 52 { 53 54 if(matrix[i][j] == 0) 55 56 matrix[0][j] = matrix[i][0] = 0; 57 58 } 59 60 } 61 62 63 64 for(int i = 1; i \u0026lt; rows; i++) 65 66 { 67 68 for(int j = 1; j \u0026lt; cols; j++) 69 70 { 71 72 if(matrix[0][j] == 0 || matrix[i][0] == 0) 73 74 matrix[i][j] = 0; 75 76 } 77 78 } 79 80 81 82 /////////////////////////////////////////////////////////////////////////// 83 84 // 最后为一行一列赋值 85 86 /////////////////////////////////////////////////////////////////////////// 87 88 if(row_has_zero) 89 90 { 91 92 for(int i = 0; i \u0026lt; cols; i++) 93 94 matrix[0][i] = 0; 95 96 } 97 98 if(col_has_zero) 99 100 { 101 102 for(int i = 0; i \u0026lt; rows; i++) 103 104 matrix[i][0] = 0; 105 106 } 107 108 } ","permalink":"https://jiat-t.github.io/leetcode/matrix/medium/set-matrix-zeroes/","summary":"矩阵置零的解题思路与 C++ 实现。","title":"矩阵置零"},{"content":"题目：搜索二维矩阵\n解法一：\n把二维矩阵转入一维数组，再对这个数组进行二分查找\n空间、时间复杂度都为O(M * N)\n1bool searchMatrix(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; matrix, int target) 2{ 3 std::vector\u0026lt;int\u0026gt; vec; 4 vec.reserve(matrix.size() * matrix[0].size()); 5 for(auto row : matrix) 6 { 7 for(auto element : row) 8 { 9 vec.push_back(element); 10 } 11 } 12 return std::binary_search(vec.begin(), vec.end(), target); 13} \u0026lt;font style=\u0026quot;background-color:#FBDE28;\u0026quot;\u0026gt;解法二：\u0026lt;/font\u0026gt; 核心公式：a[ i ]=matrix [ i / n ][ i mod n ]\n实际上并不需要再开辟一个M*N的空间用于存储二维矩阵，可以直接根据一维数组下标与行列的关系找到对应元素\n1bool searchMatrix(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; matrix, int target) 2{ 3 int rows = matrix.size(), cols = matrix[0].size(); 4 int left = 0, right = rows * cols; 5 while(left \u0026lt; right) 6 { 7 int mid = left + (right - left) / 2; 8 int x = matrix[mid / cols][mid % cols]; 9 if(target == x) 10 return true; 11 else if(x \u0026lt; target) 12 left = mid + 1; 13 else if(target \u0026lt; x) 14 right = mid; 15 } 16 return false; 17} ","permalink":"https://jiat-t.github.io/leetcode/binary-search/medium/search-a-2d-matrix/","summary":"搜索二维矩阵的解题思路与 C++ 实现。","title":"搜索二维矩阵"},{"content":"题目：颜色分类\n解法一：\n先通过一次遍历得到红、白、蓝三个颜色各自的数量，之后再通过一次O(n)的遍历（三个循环的时间复杂度加起来是O(n) )把颜色进行排序\n虽然O(2n) = O(n)，但这样还是遍历了两次，有没有只需要遍历一次的办法？\n1void sortColors(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int r = 0, w = 0, b = 0; 4 for(int num : nums) 5 { 6 switch(num) 7 { 8 case 0: r++; break; 9 case 1: w++; break; 10 case 2: b++; break; 11 } 12 } 13 for(int i = 0; i \u0026lt; r; i++) nums[i] = 0; 14 for(int j = r; j \u0026lt; r + w; j++) nums[j] = 1; 15 for(int k = r + w; k \u0026lt; r + w + b; k++) nums[k] = 2; 16} 解法二：\n","permalink":"https://jiat-t.github.io/leetcode/technique/medium/sort-colors/","summary":"颜色分类的解题思路与 C++ 实现。","title":"颜色分类"},{"content":"题目：最小覆盖子串\nminWindow（）\n1string minWindow(string s, string t) 2{ 3 if(s.size() \u0026lt; t.size()) return \u0026#34;\u0026#34;; 4 5 std::vector\u0026lt;int\u0026gt; need(128, 0); 6 std::vector\u0026lt;int\u0026gt; window(128, 0); 7 8 // 统计 t 中出现的所有字符种类数量（total_kinds） 9 int total_kinds = 0; 10 for(char c : t) 11 { 12 if(need[c] == 0) total_kinds++; 13 // 统计 t 中每个字符的出现次数 14 need[c]++; 15 } 16 17 // 窗口的左右边界（窗口是在 s 中进行滑动） 18 int left = 0, right = 0; 19 // 最小覆盖子串的起始字符以及长度 20 int start = 0, min_length = INT_MAX; 21 // 窗口中的满足 t 条件数的字符数量 22 int valid = 0; 23 while(right \u0026lt; s.size()) 24 { 25 // 提取右边界字符 26 char c = s[right]; 27 // 扩大窗口 28 right++; 29 30 // 只有当 t 中也包含当前字符时 31 // 才进行后续的“更新 window”、“判断有效字符数量”的操作 32 if(need[c] != 0) 33 { 34 // window 数组中对应的字符出现次数加 1 35 window[c]++; 36 // 当 window 中此字符的出现次数与 t 中一样时 37 // 就可以认为这个字符已经满足了条件 38 // 有效字符加 1 39 if(window[c] == need[c]) 40 valid++; 41 } 42 43 // 当所有字符都已经满足“出现次数”的条件时 44 while(valid == total_kinds) 45 { 46 // 如果此时子串长度小于之前记录的长度 47 // 就更新最小长度 48 if(right - left \u0026lt; min_length) 49 { 50 start = left; 51 min_length = right - left; 52 } 53 54 char d = s[left]; 55 // 左边界收缩 56 left++; 57 58 // 如果当前字符在 t 中出现过 59 if(need[d] != 0) 60 { 61 // 同时如果此字符是一个“满足条件”的字符 62 // 就将有效字符数减 1（因为左边界收缩之后，这个字符就被抛弃了） 63 if(window[d] == need[d]) 64 valid--; 65 // 它在窗口中的出现次数也要减 1 66 window[d]--; 67 } 68 } 69 } 70 // 如果最终 min_length 都没有被改变的话 71 // 说明根本就没有进入循环，此时直接返回空字符串 72 return min_length == INT_MAX ? \u0026#34;\u0026#34; : s.substr(start, min_length); 73} 原文：hard\n","permalink":"https://jiat-t.github.io/leetcode/substring/hard/leetcode-substring-hard/","summary":"最小覆盖子串的解题思路与 C++ 实现。","title":"最小覆盖子串"},{"content":"题目：子集\n核心思路：枚举每一个位置，并通过递归调用进入下一个位置\n具体实现：\n每次进入回溯函数时，都先将当前保存的数组存入结果数组中，之后再进入具体的回溯算法——通过循环遍历当前元素之后的每一个元素，然后再递归调用相同函数，对下一个元素进行相同操作\n1void backTrack(vector\u0026lt;int\u0026gt;\u0026amp; nums, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res, vector\u0026lt;int\u0026gt;\u0026amp; temp, int start) 2{ 3 res.emplace_back(temp); 4 for(int i = start; i \u0026lt; nums.size(); i++) 5 { 6 temp.push_back(nums[i]); 7 backTrack(nums, res, temp, i + 1); 8 temp.pop_back(); 9 } 10} 11vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; subsets(vector\u0026lt;int\u0026gt;\u0026amp; nums) 12{ 13 vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; 14 vector\u0026lt;int\u0026gt; temp; 15 res.reserve(1 \u0026lt;\u0026lt; nums.size()); 16 backTrack(nums, res, temp, 0); 17 return res; 18} ","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/subsets/","summary":"子集的解题思路与 C++ 实现。","title":"子集"},{"content":"题目：单词搜索\n使用到了深搜\n具体实现：\n首先在双层循环中判断首元素是什么，之后以这个元素为起点进行深搜\ndfs函数核心的判断逻辑是“found”变量那里——只有出现一条连续的、与word匹配的元素串，found才能为真\n要注意的是，因为同一元素不能重复使用，所以在进行深搜的时候要提前将当前元素标记为“已使用”，这里采用的是‘\\0’（不属于任何字母）\n1bool dfs(vector\u0026lt;vector\u0026lt;char\u0026gt;\u0026gt;\u0026amp; board, string word, int index, int r, int c) 2{ 3 if(index == word.size()) return true; 4 if(r \u0026lt; 0 || r \u0026gt;= board.size() || c \u0026lt; 0 || c \u0026gt;= board[0].size() || word[index] != board[r][c]) return false; 5 6 char temp = board[r][c]; 7 board[r][c] = \u0026#39;\\0\u0026#39;; 8 9 bool found = dfs(board, word, index + 1, r + 1, c ) || 10 dfs(board, word, index + 1, r, c + 1) || 11 dfs(board, word, index + 1, r - 1, c ) || 12 dfs(board, word, index + 1, r, c - 1); 13 14 board[r][c] = temp; 15 16 return found; 17} 18bool exist(vector\u0026lt;vector\u0026lt;char\u0026gt;\u0026gt;\u0026amp; board, string word) 19{ 20 for(int j = 0; j \u0026lt; board[0].size(); j++) 21 { 22 for(int i = 0; i \u0026lt; board.size(); i++) 23 { 24 if(dfs(board, word, 0, i, j)) return true; 25 } 26 } 27 return false; 28} ","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/word-search/","summary":"单词搜索的解题思路与 C++ 实现。","title":"单词搜索"},{"content":"题目：柱状图中最大的矩形\n这次与盛最多水的容器非常相似，但不同的的是十一题只需要看两边的木板长度，而这一题需要考虑容器中间的最低高度\nlargestRectangleArea（）\n1int largestRectangleArea(vector\u0026lt;int\u0026gt;\u0026amp; heights) 2{ 3 // 当右边所有矩形都比当前矩形高时 4 // 有可能不会计算以当前矩形为中心的结果 5 // 在末尾设置一个 -1（比任何矩形都要低） 6 // 可以强制触发”heights[right] \u0026lt;= heights[st.top()]“ 7 // 计算当前结果 8 heights.push_back(-1); 9 std::stack\u0026lt;int\u0026gt; st; 10 // 同理，这里是为了防止左边都高于右边的情况 11 st.push(-1); 12 int area = 0; 13 14 // 外层循环会对每一个矩形都进行一次 area 的计算 15 for(int right = 0; right \u0026lt; heights.size(); right++) 16 { 17 // 内层循环则定义具体的计算步骤 18 while(1 \u0026lt; st.size() \u0026amp;\u0026amp; heights[right] \u0026lt;= heights[st.top()]) 19 { 20 // 栈顶代表矩形的高 21 int h = st.top(); 22 st.pop(); 23 // 栈顶下面的那个数代表左边界 24 int left = st.top(); 25 // 计算面积 26 area = std::max(area, heights[h] * (right - left - 1)); 27 } 28 // 将当前的右边界作为下一次的左边界 29 st.push(right); 30 } 31 return area; 32} 原文：hard\n","permalink":"https://jiat-t.github.io/leetcode/stack/hard/leetcode-stack-hard/","summary":"柱状图中最大的矩形的解题思路与 C++ 实现。","title":"柱状图中最大的矩形"},{"content":"题目：反转链表 II\n核心思想：将 left 的前一个节点指向 right，将 left 指向 right 的下一个节点\n所以一共需要找到三个节点的位置，这里使用的是两个遍历，分别定位 left 的前一个节点 / left节点和 right 的下一个节点\nreverseBetween（）\n1ListNode* reverse(ListNode* prev, ListNode* curr, ListNode* last) 2{ 3 if(curr == last) 4 { 5 return prev; 6 } 7 ListNode* temp = curr-\u0026gt;next; 8 curr-\u0026gt;next = prev; 9 return reverse(curr, temp, last); 10} 11ListNode* reverseBetween(ListNode* head, int left, int right) 12{ 13 ListNode* dummy = new ListNode(0); 14 dummy-\u0026gt;next = head; 15 ListNode* pre = dummy; 16 17 // 找到 pre（left 的前一个节点） 18 for(int i = 1; i \u0026lt; left; i++) 19 { 20 pre = pre-\u0026gt;next; 21 } 22 // 通过 pre 可以定位到 left 23 ListNode* left_node = pre-\u0026gt;next; 24 25 // 找到 right 的下一个节点 26 ListNode* last = left_node; 27 for(int i = left; i \u0026lt;= right; i++) 28 { 29 last = last-\u0026gt;next; 30 } 31 32 // 将【left，right】区间翻转 33 // 这里的 new_head 就是之前的 right 34 ListNode* new_head = reverse(nullptr, left_node, last); 35 pre-\u0026gt;next = new_head; 36 left_node-\u0026gt;next = last; 37 38 return dummy-\u0026gt;next; 39} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/list/medium/reverse-linked-list-ii/","summary":"反转链表 II的解题思路与 C++ 实现。","title":"反转链表 II"},{"content":"题目：二叉树的中序遍历\n非常基础的一个遍历，感觉没什么好说的\u0026hellip;.\n首先确定递归函数的参数和返回值：void\u0026ndash;因为这里直接传入了一个vector的引用\n然后明确递归终止的条件：当前节点为空\n这也就意味着当前分支上的所有节点都已经遍历完了，所以应该进入下一个分支\n比如中序遍历，顺序是“左中右”，所以inorder函数中就是先递归左分支，再将中间节点压入vector，最后遍历右分支\n其他两个遍历以此类推\n1void inorder(TreeNode* root, std::vector\u0026lt;int\u0026gt;\u0026amp; traversal) 2{ 3 if(root == NULL) return; 4 inorder(root-\u0026gt;left, traversal); 5 traversal.push_back(root-\u0026gt;val); 6 inorder(root-\u0026gt;right, traversal); 7} 8 9vector\u0026lt;int\u0026gt; inorderTraversal(TreeNode* root) 10{ 11 std::vector\u0026lt;int\u0026gt; traversal; 12 13 inorder(root, traversal); 14 return traversal; 15} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/binary-tree-inorder-traversal/","summary":"二叉树的中序遍历的解题思路与 C++ 实现。","title":"二叉树的中序遍历"},{"content":"题目：验证二叉搜索树\n解法一：通过中序遍历将整棵树装入vector，再判断数组是否单调\n具体实现非常简单，但不是一个好方法，因为他需要先将所有节点的数值装入容器，再进行比较，然而实际上每次比较只需要使用到两个节点，这会造成空间的浪费\n所以应该使用“边遍历，边比较”的方法\n1void traversal(TreeNode* root, std::vector\u0026lt;int\u0026gt;\u0026amp; v) 2{ 3 if(!root) return; 4 5 traversal(root-\u0026gt;left, v); 6 7 v.push_back(root-\u0026gt;val); 8 9 traversal(root-\u0026gt;right, v); 10} 11 12bool isValidBST(TreeNode* root) 13{ 14 std::vector\u0026lt;int\u0026gt; vec; 15 traversal(root, vec); 16 17 int sz = vec.size(); 18 for(int i = 0; i \u0026lt; sz - 1; i++) 19 { 20 if(vec[i+1] \u0026lt;= vec[i]) return false; 21 } 22 return true; 23} 解法二：使用指针成员装载当前节点的父节点，并在访问当前节点时就继续比较\n这样就避免了在堆上分配内存，所有操作都只在栈上进行\n相较于上一个方法，这个方法的空间复杂度从O(n)下降到了O(h),其中h为树的最大深度\n1class Solution 2{ 3public: 4 bool isValidBST(TreeNode* root) 5 { 6 if(!root) return true; // 遍历到叶子节点之后进行剪枝，返回上一个节点 7 if(!isValidBST(root-\u0026gt;left)) return false; // 如果左子树底下的节点已经不满足规则，就没必要继续遍历了 8 if(prev \u0026amp;\u0026amp; root-\u0026gt;val \u0026lt;= prev-\u0026gt;val) return false; 9 prev = root; 10 return isValidBST(root-\u0026gt;right); // 此时左边已经全部满足规则，只需要判断右子树是否也全部满足规则即可 11 } 12private : 13 TreeNode* prev = nullptr; 14}; 补充解法：递归维护上下界\n1bool isValid(TreeNode* root, long min_val, long max_val) 2{ 3 if(root == nullptr) return true; 4 if(root-\u0026gt;val \u0026lt;= min_val || max_val \u0026lt;= root-\u0026gt;val) return false; 5 return isValid(root-\u0026gt;left, min_val, root-\u0026gt;val) \u0026amp;\u0026amp; 6 isValid(root-\u0026gt;right, root-\u0026gt;val, max_val); 7} 8bool isValidBST(TreeNode* root) 9{ 10 return isValid(root, LONG_MIN, LONG_MAX); 11} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/validate-binary-search-tree/","summary":"验证二叉搜索树的解题思路与 C++ 实现。","title":"验证二叉搜索树"},{"content":"题目：对称二叉树\n要判断一个二叉树是否对称，就是判断这个树根节点的左右子树是否能够翻转，而判断能否翻转，则需要判断这棵二叉树的内外侧节点是否相同\n因为需要拿到左右子树各自的信息，所以使用后序遍历\n首先对特殊情况进行判断：\n1.传入的左节点不为空，右节点为空，那么无法翻转\n2.左空右不空同理\n3.两边都为空时，可以翻转\n4.两遍都不为空，但是值不同，不能翻转\n5.只有当两者都不为空且值相等时，才进入递归逻辑，判断左子树的左节点与右子树的右节点能否翻转（外侧），以及左子树的右节点与右子树的左节点能否翻转（内侧）\n最后，只有当左右节点都能反转时，才可以认为这棵树 / 子树能翻转\n1bool Compare(TreeNode* left, TreeNode* right) 2 { 3 if(!left \u0026amp;\u0026amp; right) return false; 4 else if(left \u0026amp;\u0026amp; !right) return false; 5 else if(!left \u0026amp;\u0026amp; !right) return true; 6 else if(left-\u0026gt;val != right-\u0026gt;val) return false; 7 else 8 { 9 bool outside = Compare(left-\u0026gt;left, right-\u0026gt;right); 10 bool inside = Compare(left-\u0026gt;right, right-\u0026gt;left); 11 return outside \u0026amp;\u0026amp; inside; 12 } 13 } 14 bool isSymmetric(TreeNode* root) 15 { 16 return Compare(root-\u0026gt;left, root-\u0026gt;right); 17 } ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/symmetric-tree/","summary":"对称二叉树的解题思路与 C++ 实现。","title":"对称二叉树"},{"content":"题目：二叉树的层序遍历\n使用的是队列与深度优先搜索\n函数返回值为二维数组，因此需要使用多个一维数组将每一层的值存起来，而使用队列可以实现单独存储每一层的效果（FIFO）\n首先判断根节点是否为空，不为空则直接入队\n定义两个循环，外层循环用于记录每一层所有的值（相对于整棵树进行），内层循环用于将单层内的所有值一个一个的压入用于临时存储的vec中（相对于单层进行）\n通过size，可以知道每一层需要弹出的元素的数量，从而得知哪些元素是同一层的\n注：性能优化：\n1.提前将队列的首元素存储起来，避免后续对.front()函数的多次调用\n2.使用std::move()可以避免不必要的拷贝（因为vec被来就是外层循环的局部变量，当res的push操作被执行时，也就意味着这一层的循环终止了）\n1vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; levelOrder(TreeNode* root) 2{ 3 std::queue\u0026lt;TreeNode*\u0026gt; que; 4 std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; res; 5 if(root) que.push(root); 6 while(!que.empty()) 7 { 8 int size = que.size(); 9 std::vector\u0026lt;int\u0026gt; vec; 10 vec.reserve(size); 11 while(size) 12 { 13 TreeNode* curr = que.front(); 14 que.pop(); 15 16 vec.push_back(curr-\u0026gt;val); 17 if(curr-\u0026gt;left) que.push(curr-\u0026gt;left); 18 if(curr-\u0026gt;right) que.push(curr-\u0026gt;right); 19 size--; 20 } 21 res.push_back(std::move(vec)); 22 } 23 return res; 24} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/binary-tree-level-order-traversal/","summary":"二叉树的层序遍历的解题思路与 C++ 实现。","title":"二叉树的层序遍历"},{"content":"题目：二叉树的最大深度\n求深度：用前序遍历；求高度：用后序遍历\n而二叉树的最大深度，恰好就是root节点的高度，因此求root的高度即可\n至于为什么不直接用后序遍历求深度，是因为代码量相对于求高度的方法更多\n先沿着左右子树进行遍历，得到左右子树各自的最大深度，取最大值加1即可得到当前中间节点的深度\n以此类推，可以逐步累加得到根节点（最上方的中间节点）的高度\n1int maxDepth(TreeNode* root) 2{ 3 if(root == NULL) return 0; 4 int leftDepth = maxDepth(root-\u0026gt;left); 5 int rightDepth = maxDepth(root-\u0026gt;right); 6 int depth = 1 + std::max(leftDepth, rightDepth); 7 return depth; 8} 9 10// 精简版 11int maxDepth(TreeNode* root) 12{ 13 if(root == NULL) return 0; 14 return 1 + std::max(maxDepth(root-\u0026gt;left), maxDepth(root-\u0026gt;right)); 15} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/maximum-depth-of-binary-tree/","summary":"二叉树的最大深度的解题思路与 C++ 实现。","title":"二叉树的最大深度"},{"content":"题目：从前序与中序遍历序列构造二叉树\n通过递归的方法来构造二叉树，可以拆解为三个步骤： 1).找到当前根节点 \u0026ndash;\u0026gt; 2).找到左右子树的根节点 \u0026ndash;\u0026gt; 3).递归深入\n因为前序遍历会先遍历所有根节点，所以第一步只需要对数组 preorder 的下标进行递增即可\n同时根据中序遍历的特性，左子树与右子树分别会位于 inorder 数组中根节点的左右两侧，所以需要使用一种快速的方法，能够通过根节点的值直接在 inorder 数组中找到根节点对应的下标（也可以说是位置），这里选用的是哈希表。如此一来，左右子树的所有元素就也被找到了，对拆分出来的两个数组再次递归调用即可\n注：这里比较难理解的是递归时选取的左右边界\n对于左子树：\npreorder的左边界要加1，因为preorder的第一个元素是根节点，往后一个节点就是这个根节点的左子树的根节点；右边界就是左边界位置直接加上左子树元素个数\ninorder的左边界保持不变；右边界是根节点位置向前移动一位\n对于右子树：\npreorder的左边界为初始左边界加上左子树元素的个数再加一（跨过根节点），右边界保持不变\ninorder的左边界是根节点位置向后移动一位；右边界保持不变\n1class Solution 2{ 3public: 4 TreeNode* helper(vector\u0026lt;int\u0026gt;\u0026amp; preorder, vector\u0026lt;int\u0026gt;\u0026amp; inorder, int preorder_left, int inorder_left, int preorder_right, int inorder_right) 5 { 6 if(preorder_left \u0026gt; preorder_right) return nullptr; 7 8 int root_value = preorder[preorder_left]; 9 int inorder_root = index[root_value]; 10 TreeNode* root = new TreeNode(root_value); 11 12 int left_tree_size = inorder_root - inorder_left; 13 14 root-\u0026gt;left = helper(preorder, inorder, preorder_left + 1, inorder_left, preorder_left + left_tree_size, inorder_root - 1); 15 16 root-\u0026gt;right = helper(preorder, inorder, preorder_left + left_tree_size + 1, inorder_root + 1, preorder_right, inorder_right); 17 18 return root; 19 } 20 21 TreeNode* buildTree(vector\u0026lt;int\u0026gt;\u0026amp; preorder, vector\u0026lt;int\u0026gt;\u0026amp; inorder) 22 { 23 int sz = inorder.size(); 24 for(int i = 0; i \u0026lt; sz; i++) 25 { 26 index[inorder[i]] = i; 27 } 28 return helper(preorder, inorder, 0, 0, sz-1, sz-1); 29 } 30private : 31 std::unordered_map\u0026lt;int, int\u0026gt; index; 32}; ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/construct-binary-tree-from-preorder-and-inorder-traversal/","summary":"从前序与中序遍历序列构造二叉树的解题思路与 C++ 实现。","title":"从前序与中序遍历序列构造二叉树"},{"content":"题目：将有序数组转换为二叉搜索树\n先回顾一下二叉搜索树的定义：左子树的任一节点都小于根节点，右子树的任一节点都大于根节点\n现在拿到了一个有序数组，自然可以想到要从中间开始进行遍历，因此首先找到数组的中间节点，并以它作为根节点向左右进行遍历，因为数组已经有序，所以不需要再进行比较，只需要明确每一次构造的左右边界即可\n至于为什么不需要考虑奇偶数组的情况，这是因为int的除法会自动向下取整，所以即使是偶数数组，也能够保证平衡（但不是对称）\n1TreeNode* traversal(vector\u0026lt;int\u0026gt;\u0026amp; nums, int left, int right) 2{ 3 if(left \u0026gt; right) return nullptr; 4 int mid = (left + right) / 2; 5 TreeNode* root = new TreeNode(nums[mid]); 6 root-\u0026gt;left = traversal(nums, left, mid - 1); 7 root-\u0026gt;right = traversal(nums, mid + 1, right); 8 return root; 9} 10 11TreeNode* sortedArrayToBST(vector\u0026lt;int\u0026gt;\u0026amp; nums) 12{ 13 return traversal(nums, 0, nums.size() - 1); 14} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/convert-sorted-array-to-binary-search-tree/","summary":"将有序数组转换为二叉搜索树的解题思路与 C++ 实现。","title":"将有序数组转换为二叉搜索树"},{"content":"题目：有序链表转换二叉搜索树\n使用到了递归与双指针\nsortedListToBST（）\n1TreeNode* sortedListToBST(ListNode* head) 2{ 3 if(head == nullptr) return nullptr; 4 if(head-\u0026gt;next == nullptr) return new TreeNode(head-\u0026gt;val); 5 6 // 找出链表的中点 7 // 循环结束时，slow 即为树的根节点 8 // 同时 slow 左边的链表为左子树，slow 右边为右子树 9 ListNode *slow = head, *fast = head, *prev = nullptr; 10 while(fast != nullptr \u0026amp;\u0026amp; fast-\u0026gt;next != nullptr) 11 { 12 prev = slow; 13 slow = slow-\u0026gt;next; 14 fast = fast-\u0026gt;next-\u0026gt;next; 15 } 16 17 prev-\u0026gt;next = nullptr; 18 TreeNode* root = new TreeNode(slow-\u0026gt;val); 19 root-\u0026gt;left = sortedListToBST(head); 20 root-\u0026gt;right = sortedListToBST(slow-\u0026gt;next); 21 return root; 22} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/list/medium/convert-sorted-list-to-binary-search-tree/","summary":"有序链表转换二叉搜索树的解题思路与 C++ 实现。","title":"有序链表转换二叉搜索树"},{"content":"题目：平衡二叉树\n1int depth(TreeNode* curr, bool\u0026amp; res) 2{ 3 if(curr == nullptr) return 0; 4 5 int left = depth(curr-\u0026gt;left, res); 6 int right = depth(curr-\u0026gt;right, res); 7 8 if(!res) return 0; 9 10 if(std::abs(left - right) \u0026gt; 1) 11 res = false; 12 13 return std::max(left, right) + 1; 14} 15bool isBalanced(TreeNode* root) 16{ 17 bool res = true; 18 depth(root, res); 19 return res; 20} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/balanced-binary-tree/","summary":"平衡二叉树的解题思路与 C++ 实现。","title":"平衡二叉树"},{"content":"题目：二叉树展开为链表\n因为要求O(1)的空间复杂度，因此不能再使用额外的容器，所以这里使用了一个原地算法\n思路如下： 最终结果是将原来的树退化为了一条沿着右子树不断延伸的链表，根据结果不难发现：越是靠右的节点，在链表中的位置越靠前；而越是靠左的节点，在链表中的位置越往后\n因此，可以通过迭代，每次将父节点的整棵左子树插入父节点与右子树之间\n具体实现：\n先确定迭代终止条件：当前节点为空\n之后判断当前节点是否存在左节点（因为需要做的就是将左子树插入），如果左子树不存在，就可以认为此时位于最左边的节点，也就是此时这一支已经满足了单链表要求；然后就可以对右子树进行遍历，当到达右子树最后一个结点之后，就把父节点的整颗右子树挂在当前节点的右枝，再将根节点的左子树完整的移动到右子树，左子树置空即可\n1void flatten(TreeNode* root) 2{ 3 while(root) 4 { 5 if(root-\u0026gt;left) 6 { 7 auto temp = root-\u0026gt;left; 8 while(temp-\u0026gt;right) temp = temp-\u0026gt;right; 9 temp-\u0026gt;right = root-\u0026gt;right; 10 root-\u0026gt;right = root-\u0026gt;left; 11 root-\u0026gt;left = nullptr; 12 } 13 root = root-\u0026gt;right; 14 } 15} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/flatten-binary-tree-to-linked-list/","summary":"二叉树展开为链表的解题思路与 C++ 实现。","title":"二叉树展开为链表"},{"content":"题目：杨辉三角\n[1]\n[1,1]\n[1,2,1]\n[1,3,3,1]\n[1,4,6,4,1]\n把左端对齐，以符合二维数组的格式\n根据杨辉三角的定义，可以知道，当前元素的值，等于左上方元素加正上方的元素，也就是\nv[i][j] = v[i-1][j-1] + v[i-1][j]\n又因为每一层的大小都比上一层多一个，所以每次循环都需要对数组向后扩容一位，并将所有元素初始化为1** **\n1vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; generate(int numRows) 2 { 3 std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; vec(numRows); 4 for(int i = 0; i \u0026lt; numRows; i++) 5 { 6 vec[i].resize(i+1, 1); 7 for(int j = 1; j \u0026lt; i; j++) 8 { 9 vec[i][j] = vec[i-1][j-1] + vec[i-1][j]; 10 } 11 } 12 return vec; 13 } ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/easy/pascals-triangle/","summary":"杨辉三角的解题思路与 C++ 实现。","title":"杨辉三角"},{"content":"题目：买卖股票的最佳时机\n思路：\n所谓贪心算法，就是每个步骤都挑选局部最优解，从而最终达到全局最优解的效果\n在这题中，”贪心“体现在——在遍历过程中，每次都只将当前价格与之前遇到的最低价格进行利润计算\n这样就可以避免不必要的计算\n具体实现如下：\n1int maxProfit(vector\u0026lt;int\u0026gt;\u0026amp; prices) 2{ 3 if(prices.empty()) return 0; 4 int max_profit = 0, min_price = prices[0]; 5 for(int price : prices) 6 { 7 if(price \u0026lt; min_price) 8 min_price = price; 9 else 10 { 11 int curr_profit = price - min_price; 12 max_profit = curr_profit \u0026lt; max_profit ? max_profit : curr_profit; 13 } 14 } 15 return max_profit; 16} ","permalink":"https://jiat-t.github.io/leetcode/greedy-algorithm/easy/best-time-to-buy-and-sell-stock/","summary":"买卖股票的最佳时机的解题思路与 C++ 实现。","title":"买卖股票的最佳时机"},{"content":"题目：买卖股票的最佳时机 II\n参考：https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/solutions/12625/best-time-to-buy-and-sell-stock-ii-zhuan-hua-fa-ji/?envType=problem-list-v2\u0026amp;envId=91oI3WTD\n对于连续上涨的情况，第一天买，最后一天卖，可以使得利润最大化，此时利润 profit 等于 p_n - p_1，等价于（p_n - p_n-1) + (p_n-1 - p_n-2) + \u0026hellip;. + (p_2 - p_1)，也就是说，可以将其分解为前一天买，下一天买，即每天都进行交易\n对于连续下跌的情况，不进行任何交易（即利润为零）可以使得利润最大；\nmaxProfit（）\n1int maxProfit(vector\u0026lt;int\u0026gt;\u0026amp; prices) 2{ 3 int res = 0; 4 // 只遍历一遍数组 5 for(int i = 1; i \u0026lt; prices.size(); i++) 6 { 7 // 计算两天之间的利润 8 int temp = prices[i] - prices[i - 1]; 9 // 只有当利润大于零时才记入总利润 10 if(0 \u0026lt; temp) res += temp; 11 } 12 return res; 13} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/greedy-algorithm/medium/best-time-to-buy-and-sell-stock-ii/","summary":"买卖股票的最佳时机 II的解题思路与 C++ 实现。","title":"买卖股票的最佳时机 II"},{"content":"题目：二叉树中的最大路径和\n思路：对于任意一个节点，有两件不同的事要做：\n作为贡献值（向上传递）：此时，它只能选择【当前节点值】与【当前节点值 + 左子树贡献值 + 右子树贡献值】中的更大者作为拐点（更新全局最大路径和）： 以当前节点为根的局部最大路径和为——当前节点值 + 左子树最大贡献 + 右子树最大贡献，如果之前记录过的最大和小于这个临时的和，就将其覆写 maxPathSum（）\n1class Solution 2{ 3public: 4 int maxPathSum(TreeNode* root) 5 { 6 pathGain(root); 7 return maxSum; 8 } 9private : 10 int maxSum = INT_MIN; 11 int pathGain(TreeNode* curr) 12 { 13 if(curr == nullptr) return 0; 14 15 // 计算左右子树的最大贡献（不小于零） 16 int left_gain = std::max(pathGain(curr-\u0026gt;left), 0); 17 int right_gain = std::max(pathGain(curr-\u0026gt;right), 0); 18 19 // 以当前节点为根的最大路径之和 20 int curr_sum = curr-\u0026gt;val + left_gain + right_gain; 21 22 // 更新最大值 23 maxSum = std::max(maxSum, curr_sum); 24 25 // 向上传递 26 return curr-\u0026gt;val + std::max(left_gain, right_gain); 27 } 28}; 原文：hard\n","permalink":"https://jiat-t.github.io/leetcode/binary-tree/hard/leetcode-binary-tree-hard/","summary":"二叉树中的最大路径和的解题思路与 C++ 实现。","title":"二叉树中的最大路径和"},{"content":"题目：验证回文串\nisPalindrome（）\n1bool isPalindrome(string s) 2{ 3 if(s.size() == 1) return true; 4 // 定义左右指针 5 int left = 0, right = s.size() - 1; 6 // 不需要考虑 left = right 的情况 7 // 因为中间多一个字符的情况可以直接当作偶数串处理 8 while(left \u0026lt; right) 9 { 10 // 拿到左边的数字或字符 11 while(left \u0026lt; right \u0026amp;\u0026amp; !isalnum(s[left])) 12 { 13 left++; 14 } 15 16 // 拿到右边的数字或字符 17 while(left \u0026lt; right \u0026amp;\u0026amp; !isalnum(s[right])) 18 { 19 right--; 20 } 21 22 // 如果是大写就将其转换为小写 23 // 如果已经是小写或数字就原样返回 24 if(std::tolower(s[left]) != std::tolower(s[right])) return false; 25 // 移动指针 26 left++;right--; 27 } 28 return true; 29} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/double-pointers/easy/valid-palindrome/","summary":"验证回文串的解题思路与 C++ 实现。","title":"验证回文串"},{"content":"题目：最长连续序列\n使用到了unordered_set，它只存储key，逻辑意义是“这个元素是否存在于集合中？”\n具体思路：对于一个元素，首先判断他的上一个元素（当前值减去1）是否在集合中，\n- 如果在，就意味着当前元素不是起始位置，不需要管他，直接进行下一次循环； - 如果不在，代表着当前元素是起始位置，然后从他开始向后遍历，直到下一个元素不存在于集合中，此时得到的长度即为连续序列的长度 每一层循环都对res进行一次比较，如果当前长度更大，就替换之前的res\n1int longestConsecutive(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int res = 0; 4 std::unordered_set\u0026lt;int\u0026gt; seqs(nums.begin(), nums.end()); 5 for(auto num : seqs) 6 { 7 int temp = 1; 8 if(seqs.contains(num - 1)) 9 continue; 10 while(seqs.contains(num + 1)) 11 { 12 temp++; 13 num++; 14 } 15 res = std::max(temp, res); 16 } 17 return res; 18} ","permalink":"https://jiat-t.github.io/leetcode/hash/medium/longest-consecutive-sequence/","summary":"最长连续序列的解题思路与 C++ 实现。","title":"最长连续序列"},{"content":"题目：分割回文串\n思路：对于每一个字符，都先判断其是否能够被分割，即以start为首，以当前字符为尾的字符串是否为回文串；如果能够分割（第一条路径），就将当前元素压入path，组成子回文串，并从下一个字符开始进行新一轮的回文串判断；第二条路径为：不从当前字符进行分割，而是直接转向下一个字符，从下一个字符开始判断（为了找到更长的回文串）\n1bool isPalindrome(const string\u0026amp; s, int left, int right) 2{ 3 while(left \u0026lt; right) 4 { 5 if(s[left++] != s[right--]) 6 return false; 7 } 8 return true; 9} 10void dfs(string\u0026amp; s, int index, int start, vector\u0026lt;string\u0026gt;\u0026amp; path, vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt;\u0026amp; res) 11{ 12 const int sz = s.size(); 13 if(index == sz) 14 { 15 res.emplace_back(path); 16 return; 17 } 18 19 if(isPalindrome(s, start, index)) 20 { 21 path.emplace_back(s.substr(start, index - start + 1)); 22 dfs(s, index + 1, index + 1, path, res); 23 path.pop_back(); 24 } 25 26 if(index \u0026lt; sz - 1) 27 dfs(s, index + 1, start, path, res); 28} 29 30vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; partition(string s) 31{ 32 vector\u0026lt;string\u0026gt; path; 33 vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; res; 34 35 dfs(s, 0, 0, path, res); 36 return res; 37} ","permalink":"https://jiat-t.github.io/leetcode/back-track/medium/palindrome-partitioning/","summary":"分割回文串的解题思路与 C++ 实现。","title":"分割回文串"},{"content":"题目：只出现一次的数字\n题目要求的是线性时间复杂度以及常量额外空间，所以像哈希表之类的解法都不能使用\n这里使用到的是**异或（XOR）**，它的性质是：\n1.任何数与0异或得到这个数本身\n2.任何数与自身异或得到0\n3.异或运算满足结合律，即_a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b_\n通过以上性质，只需要将数组内的所有元素放在同一个异或运算式中，就可以将所有相同的数字进行组合（结果为零），又因为性质一，所以最后得到的结果一定就是落单的数字\n1int singleNumber(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int res = 0; 4 for(auto num : nums) res ^= num; 5 return res; 6} ","permalink":"https://jiat-t.github.io/leetcode/technique/easy/single-number/","summary":"只出现一次的数字的解题思路与 C++ 实现。","title":"只出现一次的数字"},{"content":"题目：随机链表的复制\n使用到的是哈希表\n思路：因为第一次遍历复制各个结点的时候，是不知道后面的节点的，因此一次遍历无法将random元素对应上去，所以一共需要两次遍历：第一次复制所有节点并并对应的random与当前节点绑定，第二次将当前节点的random元素指向对应的成员\n1Node* copyRandomList(Node* head) 2 { 3 if(head == nullptr) return nullptr; 4 Node* curr = head; 5 std::unordered_map\u0026lt;Node*, Node*\u0026gt; map; 6 7 while(curr != nullptr) 8 { 9 map[curr] = new Node(curr-\u0026gt;val); 10 curr = curr-\u0026gt;next; 11 } 12 13 curr = head; 14 while(curr != nullptr) 15 { 16 map[curr]-\u0026gt;next = map[curr-\u0026gt;next]; 17 map[curr]-\u0026gt;random = map[curr-\u0026gt;random]; 18 curr = curr-\u0026gt;next; 19 } 20 return map[head]; 21 } ","permalink":"https://jiat-t.github.io/leetcode/list/medium/copy-list-with-random-pointer/","summary":"随机链表的复制的解题思路与 C++ 实现。","title":"随机链表的复制"},{"content":"题目：单词拆分\n1. 确定状态：dp[ i ] 代表前 i 个字符组成的字符串是否能够被拆分 2. 状态转移：dp[ i ] = (**dp[ j ]** \u0026amp;\u0026amp; **s.substr(j, i - j)**也能够被拆分)，其中 j 为区间 [ 0, i ] 的任意整数 具体实现：\n首先将字典转换为无序集合，提高查找速度；原字符串拷贝至string_view类型，以实现零开销访问范围内的部分字符串；最后定义dp数组，用来记录前 i 个字符能否被拆分，注意这里的首位元素为真，因为空字符串肯定是能够被拆分的，其他位默认为假\n进入循环——外层循环遍历原字符串，内层循环遍历字典；之后根据上述的 b 条件，确定字符串前i个元素是否能拆分\n1bool wordBreak(string s, vector\u0026lt;string\u0026gt;\u0026amp; wordDict) 2{ 3 std::unordered_set\u0026lt;string_view\u0026gt; dict(wordDict.begin(), wordDict.end()); 4 std::string_view sv = s; 5 std::vector\u0026lt;bool\u0026gt; dp(s.size() + 1, false); 6 dp[0] = true; 7 8 for(int i = 1; i \u0026lt;= s.size(); i++) 9 { 10 for(int j = 0; j \u0026lt; i; j++) 11 { 12 if(dp[j] \u0026amp;\u0026amp; dict.find(sv.substr(j, i - j)) != dict.end()) 13 { 14 dp[i] = true; 15 break; 16 } 17 } 18 } 19 return dp[s.size()]; 20} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/word-break/","summary":"单词拆分的解题思路与 C++ 实现。","title":"单词拆分"},{"content":"题目：环形链表\n使用的是双指针（快慢指针）\n如果链表中此存在环的话，那么两个指针就一定会在这个环内循环，也就一定会相遇，因此可以将两个指针相等作为链表中存在环的判断条件（至于为什么会相遇：这里令慢指针的速度为1，快指针速度为2，两者的相对速度为1，所以在进入环之后，相当于是快指针在以1的速度追慢指针，之后一定会追上）\n注：这里的两个if一定要先判断head与fast是否为空！！！！！\n1bool hasCycle(ListNode *head) 2 { 3 if(head == nullptr || head-\u0026gt;next == nullptr) return false; 4 ListNode *slow = head, *fast = head-\u0026gt;next; 5 while(slow != fast) 6 { 7 if( fast == nullptr || fast-\u0026gt;next == nullptr) return false; 8 slow = slow-\u0026gt;next; 9 fast = fast-\u0026gt;next-\u0026gt;next; 10 } 11 return true; 12 } ","permalink":"https://jiat-t.github.io/leetcode/list/easy/linked-list-cycle/","summary":"环形链表的解题思路与 C++ 实现。","title":"环形链表"},{"content":"题目：环形链表 II\n如图，环外的长度为a，slow指针进入环内走了b才与fast相遇，fast走完了n圈，也就是n（b+c）+ b的距离\n根据两者相遇时的时间相同，可以得到等式：a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)\n当n等于1时，a=c；此时如果放置一个指针从head处向后移动，同时另一个指针从slow与fast相遇的地方以相同的速度沿着c移动，那么两者最终会在环的入口处相遇；至于为什么不考虑n，而是直接取n为1，是因为n仅仅代表着相遇处的指针多走的圈数，最后两者还是会在入口相遇\n1ListNode *detectCycle(ListNode *head) 2 { 3 if(head == nullptr || head-\u0026gt;next == nullptr) return nullptr; 4 ListNode *slow = head, *fast = head, *p1 = head; 5 while(fast != nullptr) 6 { 7 slow = slow-\u0026gt;next; 8 if(fast-\u0026gt;next == nullptr) return nullptr; 9 fast = fast-\u0026gt;next-\u0026gt;next; 10 if(slow == fast) 11 { 12 while(p1 != slow) 13 { 14 p1 = p1-\u0026gt;next; 15 slow = slow-\u0026gt;next; 16 } 17 return p1; 18 } 19 } 20 return nullptr; 21 } ","permalink":"https://jiat-t.github.io/leetcode/list/medium/linked-list-cycle-ii/","summary":"环形链表 II的解题思路与 C++ 实现。","title":"环形链表 II"},{"content":"题目：LRU 缓存\n使用到的是哈希表和链表（重点是链表迭代器）\n这题难点在于“逐出最久未使用的关键字”，首先明确思路：既然要求常数时间的复杂度，那么就一定会要用到哈希表（查找），但是又要求有一个数据结构可以存储元素使用的顺序，并且改变顺序时也要是O(1)的时间复杂度，那么就也要使用到链表（通过调用 Splice函数 可以改变迭代器位置，从而在常数时间内实现顺序的改变）\n但是如何将哈希表与链表结合起来？答：将value和iterator绑定在一起，这样就通过key就既可以得到value的值，又可以实现iterator的移动\n具体实现：\n首先拿到最大容量，避免后续多次函数调用产生的开销；\n然后开始定义 get 函数：\n- 如果哈希表内能够找到对应的key，那么就认为这个元素被访问了一次，因此也就需要将其放在链表的尾部，这里可以通过key在哈希表中找到对应的iterator - splice（）函数的语法之一（移动单个节点）： * **void splice(const_iterator pos, list\u0026amp; other, const_iterator it);** - 将 `other` 链表中由 `it` 指向的那个元素剪切到 `this` 链表的 `pos` 位置 最后实现 put 函数：\n- 这里有两种情况需要处理：1.当前key已经存在\t2.容量已满 - 1.key重复： * 意味着需要覆写之前的value，将其移至list尾部，这里还是使用到splice（） - 2.容量已满： * 此时需要删除头节点，直接pop_front()即可；同时也要同步删除哈希表中对应的元素 最后将新插入的元素添加到两个容器即可\n1class LRUCache 2{ 3public: 4 LRUCache(int capacity) : capacity(capacity) 5 { 6 cache.reserve(capacity); 7 } 8 9 int get(int key) 10 { 11 if(cache.contains(key)) 12 { 13 auto it = cache.find(key); 14 order.splice(order.end(), order, it-\u0026gt;second.second); 15 return it-\u0026gt;second.first; 16 } 17 else return -1; 18 } 19 20 void put(int key, int value) 21 { 22 auto it = cache.find(key); 23 if(it != cache.end()) // 如果value已经存在，则更新 24 { 25 it-\u0026gt;second.first = value; 26 order.splice(order.end(), order, it-\u0026gt;second.second); 27 return; 28 } 29 if(cache.size() \u0026gt;= capacity) // 处理超出容量情况 30 { 31 cache.erase(order.front()); 32 order.pop_front(); 33 } 34 35 order.push_back(key); 36 cache[key] = {value, std::prev(order.end())}; 37 } 38 39private : 40 int capacity; 41 std::unordered_map\u0026lt;int, std::pair\u0026lt;int, std::list\u0026lt;int\u0026gt;::iterator\u0026gt;\u0026gt; cache; // 键，值，迭代器 42 std::list\u0026lt;int\u0026gt; order; 43}; ","permalink":"https://jiat-t.github.io/leetcode/list/medium/lru-cache/","summary":"LRU 缓存的解题思路与 C++ 实现。","title":"LRU 缓存"},{"content":"题目：排序链表\n解法一：归并排序（分治）\n时间复杂度：O(nlogn)，空间复杂度：O(logn)\n从上至下将链表拆分为长度为一的单链表，然后先对长度为1的链表排序，合并两个链表；\n再对长度为2的两个链表排序，之后合并\u0026hellip;.以此类推，每次排序并合并一半链表，最终就可以得到有序的链表\n具体实现：\nmergeSort（）：\n在头部定义一个哨兵节点，然后遍历比较left与right的值，如果left更小，就将当前left节点接到哨兵节点后面，并将left向后移动一位，继续比较（right同理）；最后当left或者right其中一个为空，也就是该子链表遍历完成时，就直接将另一个链表接到末尾，此时返回的哨兵节点的next就是排序后的链表的头节点\nsortList（）： 在主函数中，需要不断地将链表分为两半（通过递归调用），之后每一层递归都会调用mergeSort函数对当前分割出来的两个链表进行排序，直到最后合成回初始的链表\n解法二：归并排序（迭代）\nps：这里我还没看明白，之后再补上\u0026hellip;\u0026hellip;\u0026hellip;\u0026hellip;\u0026hellip;\u0026hellip;..\n1class Solution 2{ 3public: 4 ListNode* mergeSort(ListNode* left, ListNode* right) 5 { 6 ListNode* dummyNode = new ListNode(0); 7 ListNode* trail = dummyNode; 8 while(left \u0026amp;\u0026amp; right) 9 { 10 if(left-\u0026gt;val \u0026lt; right-\u0026gt;val) 11 { 12 trail-\u0026gt;next = left; 13 left = left-\u0026gt;next; 14 } 15 else 16 { 17 trail-\u0026gt;next = right; 18 right = right-\u0026gt;next; 19 } 20 trail = trail-\u0026gt;next; 21 } 22 if(left) 23 trail-\u0026gt;next = left; 24 else 25 trail-\u0026gt;next = right; 26 27 return dummyNode-\u0026gt;next; 28 } 29 30 ListNode* sortList(ListNode* head) 31 { 32 if(!head || !head-\u0026gt;next) return head; 33 34 ListNode *slow = head, *fast = head-\u0026gt;next; 35 while(fast \u0026amp;\u0026amp; fast-\u0026gt;next) 36 { 37 slow = slow-\u0026gt;next; 38 fast = fast-\u0026gt;next-\u0026gt;next; 39 } 40 ListNode *second = slow-\u0026gt;next; 41 slow-\u0026gt;next = nullptr; 42 ListNode *first = head; 43 44 first = sortList(first); 45 second = sortList(second); 46 47 return mergeSort(first, second); 48 } 49}; ","permalink":"https://jiat-t.github.io/leetcode/list/medium/sort-list/","summary":"排序链表的解题思路与 C++ 实现。","title":"排序链表"},{"content":"题目：乘积最大子数组\n确定状态：dp[ i ] 代表“以当前元素为结尾的最大/最小乘积是多少”\n状态转移：（具体公式见下图）一开始的想法是——如果当前元素乘上dp数组的上一个元素之后，所得乘积比上一个还要小，那么就以当前元素为起点向后乘；反之，将所得乘积作为以当前元素（nums[ i ]）为结尾的连续乘积存入dp数组\n但是这会导致一个问题——如果原数组是 [ -2, 3, -4 ] 这种类型的话，期望结果是24（因为负负得正），但是上一个方法却会直接抛弃-2 * 3的结果，转而从 -4 开始重新计数，又因为此时已经到达了末尾，此时dp数组为 [-2, -6, -4 ] 所以最后会返回-2\n上一种方法相当于只记录了最大正乘积，没有考虑到最小负乘积可能会与负数相乘，因此还需要定义一个dp数组，用于存放最小乘积\n1int maxProduct(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 if(nums.size() == 1) return nums[0]; 4 std::vector\u0026lt;int\u0026gt; dp_max(nums.size(), 1), dp_min(nums.size(), 1); 5 dp_max[0] = dp_min[0] = nums[0]; 6 7 int res = nums[0]; 8 for(int i = 1; i \u0026lt; nums.size(); i++) 9 { 10 int choice1 = dp_max[i -1] * nums[i]; 11 int choice2 = dp_min[i -1] * nums[i]; 12 13 dp_max[i] = std::max({nums[i], choice1, choice2}); 14 dp_min[i] = std::min({nums[i], choice1, choice2}); 15 16 res = res \u0026lt; dp_max[i] ? dp_max[i] : res; 17 } 18 return res; 19} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/maximum-product-subarray/","summary":"乘积最大子数组的解题思路与 C++ 实现。","title":"乘积最大子数组"},{"content":"题目：寻找旋转排序数组中的最小值\n可以将旋转后的数组视为两个并排的上升台阶，前一部分是“高位台阶”，后一部分是“低位台阶”（只有当旋转nums.size()的整数倍时才会出现前低后高的情况）\n高位台阶中的每一级都大于低位台阶的最大级，也就是数组的最后一位；因此，如果 nums.back() \u0026lt; nums[mid] 的话，就可以认为最小值一定在 mid 右边；反之，nums[mid] \u0026lt;= nums.back() 的话，最小值就在 mid 左边 或 nums[mid] 自身就是最小值\n根据这一特性，只需要比较 x 和 nums[n−1] 的大小关系，就间接地知道了 x 和数组最小值的位置关系，从而不断地缩小数组最小值所在位置的范围，二分找到数组最小值\n1int findMin(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int left = 0, right = nums.size(); 4 while(left \u0026lt; right) 5 { 6 int mid = left + (right - left) / 2; 7 if(nums[mid] \u0026lt;= nums.back()) 8 right = mid; 9 else 10 left = mid + 1; 11 } 12 return nums[left]; 13} ","permalink":"https://jiat-t.github.io/leetcode/binary-search/medium/find-minimum-in-rotated-sorted-array/","summary":"寻找旋转排序数组中的最小值的解题思路与 C++ 实现。","title":"寻找旋转排序数组中的最小值"},{"content":"题目：最小栈\n对这个类需要完成的任务有五个：\n1.初始化\t2.入栈\t3.出栈\t4.获取栈顶元素\t5.获取最小值\n因为初始的时候没有元素需要初始化，所以这个类的构造函数不需要去管他\n通过在这个类中构建一个STL自带的stack，就可以实现234的操作，而5则需要再使用一个辅助栈，这题的难点也是在5\n题目要求在常数时间内检索到最小值，所以一个一个去遍历肯定是不行的，这也是引入辅助栈的原因，我们可以通过不断地将更小的元素压入这个辅助栈（minst），来达到”栈顶的元素肯定是最小“的条件；之后如果需要取出最小元素的话，只需要对minst进行一次出栈操作即可\n具体实现：\n1.当有元素传入时，如果此时minst为空，就直接入栈，此后入栈的元素都是当前栈顶元素与传入元素的更小值\n2.出栈操作也应该是两者同时进行的，因为最小值需要根据st的元素动态更新（假如此时st的栈顶就是最小元素，而下一步操作就是出栈，那么之后栈中的最小元素也应该发生变化）\n1class MinStack 2{ 3public: 4 std::stack\u0026lt;int\u0026gt; minst; 5 std::stack\u0026lt;int\u0026gt; st; 6 7 MinStack() 8 { 9 } 10 11 void push(int val) 12 { 13 st.push(val); 14 if(minst.empty()) minst.push(val); 15 else minst.push(std::min(val, minst.top())); 16 } 17 18 void pop() 19 { 20 minst.pop(); 21 st.pop(); 22 } 23 24 int top() 25 { 26 return st.top(); 27 } 28 29 int getMin() 30 { 31 return minst.top(); 32 } 33}; ","permalink":"https://jiat-t.github.io/leetcode/stack/medium/min-stack/","summary":"最小栈的解题思路与 C++ 实现。","title":"最小栈"},{"content":"题目：库存管理 III\n使用内置的函数 nth_element（）\n难绷\ninventoryManagement（）\n1vector\u0026lt;int\u0026gt; inventoryManagement(vector\u0026lt;int\u0026gt;\u0026amp; stock, int cnt) 2{ 3 if(stock.empty() || cnt == 0) return {}; 4 5 std::nth_element(stock.begin(), stock.begin() + cnt, stock.end()); 6 7 std::vector\u0026lt;int\u0026gt; res(stock.begin(), stock.begin() + cnt); 8 return res; 9} 原文：easy\n","permalink":"https://jiat-t.github.io/leetcode/heap/easy/leetcode-heap-easy/","summary":"库存管理 III的解题思路与 C++ 实现。","title":"库存管理 III"},{"content":"题目：相交链表\n设节点指针A，B分别指向两个链表的头节点，C为首个公共节点\n链表A，B的长度分别为a，b，其中公共长度为c\n因此，第一次从A走到C的距离是a-c，从B走到C的距离是b-c\n当指针A先遍历完链表A，再遍历链表B，直到第二次遇到公共节点C，一共走了a+（b-c）的距离\n当指针B先遍历完链表B，再遍历链表A，直到第二次遇到公共节点C，一共走了b+（a-c）的距离\n此时可以发现两者所走过的距离相等，因此可以利用这个条件找到第一个公共节点C：\n构造while循环，只要A与B指针不相同，就继续向后遍历，因为最后走的距离相同，也就是循环的次数相同，所以在同一个循环中进行两者的遍历即可\n1ListNode* getIntersectionNode(ListNode *headA, ListNode *headB) 2 { 3 ListNode *A = headA, *B = headB; 4 while(A != B) 5 { 6 A = A != nullptr ? A-\u0026gt;next : headB; 7 B = B != nullptr ? B-\u0026gt;next : headA; 8 } 9 return A; 10 } ","permalink":"https://jiat-t.github.io/leetcode/list/easy/intersection-of-two-linked-lists/","summary":"相交链表的解题思路与 C++ 实现。","title":"相交链表"},{"content":"题目：比较版本号\n这题的核心是：以每一个‘.‘作为分界线，将 string 转换为 int，逐段进行比较，如果相同，就进入下一段进行比较；如果不同，则根据数值的大小选择返回值\ncompareVersion（）\n1int compareVersion(string version1, string version2) 2{ 3 // 定义两个版本各自的指针 4 int i = 0, j = 0; 5 int n1 = version1.size(), n2 = version2.size(); 6 7 while(i \u0026lt; n1 || j \u0026lt; n2) 8 { 9 int num1 = 0, num2 = 0; 10 11 // 将 v1 转化为 int，直到遇到’.‘ 12 while(i \u0026lt; n1 \u0026amp;\u0026amp; version1[i] != \u0026#39;.\u0026#39;) 13 { 14 num1 = num1 * 10 + (version1[i] - \u0026#39;0\u0026#39;); 15 i++; 16 } 17 // 将 v2 转化为 int，直到遇到’.‘ 18 while(j \u0026lt; n2 \u0026amp;\u0026amp; version2[j] != \u0026#39;.\u0026#39;) 19 { 20 num2 = num2 * 10 + (version2[j] - \u0026#39;0\u0026#39;); 21 j++; 22 } 23 24 // 如果不等，直接返回 25 if(num1 != num2) 26 { 27 return num1 \u0026lt; num2 ? -1 : 1; 28 } 29 30 // 跳过’.\u0026#39;并进入下一段 31 i++; 32 j++; 33 } 34 // 如果在 while 循环中没有返回 35 // 就说明两个版本号相同 36 return 0; 37} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/double-pointers/medium/compare-version-numbers/","summary":"比较版本号的解题思路与 C++ 实现。","title":"比较版本号"},{"content":"题目：多数元素\n解法一（哈希表）：\n第一次遍历将数组中的元素存入哈希表，nums自身的数值作为键，出现次数作为值\n之后再通过一次遍历找出表中值大于n/2对应的键，作为 _多数元素 _返回\n1int majorityElement(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2 { 3 int sz = nums.size(); 4 int res; 5 std::unordered_map\u0026lt;int, int\u0026gt; um; 6 for(int i = 0; i \u0026lt; sz; i++) 7 { 8 if(um.find(nums[i]) == um.end()) 9 um[nums[i]] = 1; 10 else 11 um[nums[i]]++; 12 } 13 for(auto num : nums) 14 { 15 if(um[num] \u0026gt; static_cast\u0026lt;float\u0026gt;(sz / 2)) 16 res = num; 17 } 18 return res; 19 } 解法二：\n这里借用一下灵神的比喻：169. 多数元素 - 力扣（LeetCode）\n用「擂台赛」打比方：\n想象一众武林高手比武，谁会笑到最后？\n擂主登场：nums[0] 成为初始擂主，生命值为 1。\n挑战者出现：遍历后续元素，作为挑战者。\n比武：如果挑战者与擂主属于同一门派（值相同），那么擂主生命值加 1，否则擂主生命值减 1。\n擂主更迭：如果比武后，擂主生命值降为 0（同归于尽），那么下一个挑战者成为新的擂主，生命值为 1。\n最后在擂台上的那人，便是武林盟主（绝对众数）。\n1int majorityElement(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int res = 0, hp = 0; 4 for(int x : nums) 5 { 6 if(hp == 0) 7 { 8 res = x; 9 hp = 1; 10 } 11 else 12 hp += res == x ? 1 : -1; 13 } 14 return res; 15} ","permalink":"https://jiat-t.github.io/leetcode/technique/easy/majority-element/","summary":"多数元素的解题思路与 C++ 实现。","title":"多数元素"},{"content":"题目：轮转数组\n解法一：\n思路比较简单，仅仅是将移动后的元素存放到另一个数组\n具体实现：\n因为轮转的特性，每当移动的次数是数组大小的整数倍时，就相当于完全没有变化，使所以直接返回\n之后再通过一轮遍历，将原数组中的元素进行移动即可\nrotate（）\n1// 解法一： 2void rotate(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 3{ 4 int sz = nums.size(); 5 if(k % sz == 0) return; 6 7 std::vector\u0026lt;int\u0026gt; vec(sz, 0); 8 for(int i = 0; i \u0026lt; sz; i++) 9 { 10 int index = (i + k) % sz; 11 vec[index] = nums[i]; 12 } 13 nums = vec; 14} 解法二：\nPS：突然发现我可能是个傻逼😅😅😅😅😅😅😅😅😅😅😅😅😅😅\n参考了这里的解法：189. 轮转数组 - 力扣（LeetCode）\n思路如下：\n假设现在有一个数组[1,2,3,4,5,6,7]，需要变换成[5,6,7,1,2,3,4]\n可以将转换后的数组视为：[5,6,7] + [1,2,3,4]\n首先要确保的是[5,6,7]在[1,2,3,4]前面，这可以通过翻转整个数组得到——\u0026gt; [7,6,5,4,3,2,1]\n之后就只需要对[7,6,5] 和 [4,3,2,1]两个子数组进行翻转就可以得到最终的结果\n如此，即可将复杂的移动操作转换为反转操作\nrotate（）\n1// 解法二： 2void rotate(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 3{ 4 k %= nums.size(); 5 ranges::reverse(nums); 6 std::reverse(nums.begin(), nums.begin() + k); 7 std::reverse(nums.begin() + k, nums.end()); 8} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/array/medium/rotate-array/","summary":"轮转数组的解题思路与 C++ 实现。","title":"轮转数组"},{"content":"题目：打家劫舍\n1.状态定义：\ndp[i]指的是”到达第i个房子时，目前为止能偷到的最大金额是多少“\n2.转移方程：\n用一个例子来解释——\n假如当前已经偷完了前3栋房子，来到了第四栋房子（里面有num元）\n那么现在就有两个选择：\n1).不偷第四栋房子，那么金额就是dp[3]（也就是前三栋房子的最优结果）\n2).偷第四栋房子，那么总金额就是dp[i-1] + num（前两栋房子的最优结果加上第四栋房子）\n因此，前四栋房子的最大金额就是两者中的更大者\n1int rob(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int pre = 0, curr = 0; 4 int temp; 5 for(int num : nums) 6 { 7 temp = curr; 8 curr = std::max(pre + num, curr); 9 pre = temp; 10 } 11 return curr; 12} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/house-robber/","summary":"打家劫舍的解题思路与 C++ 实现。","title":"打家劫舍"},{"content":"题目：岛屿数量\n使用的是深度优先搜素\n主要思路：对输入的二维数组进行遍历，如果遇到一，则认为遇到了一个新的岛屿，然后对这个1周围上下左右四个元素进行搜索；如果周围元素仍然是1，就可以认为它与上一个区域位于同一个岛屿，并再次以当前这个一为起点，调用DFS函数递归搜索\nDFS函数的终止条件是遇到零，因此每当遍历完成一个‘1’时，都要将其设置为‘0’，避免重复搜索导致栈溢出\nnumIslands（）\n1class Solution 2{ 3public: 4 void DFS(vector\u0026lt;vector\u0026lt;char\u0026gt;\u0026gt;\u0026amp; grid, int x, int y) 5 { 6 for(int k = 0; k \u0026lt; 4; k++) 7 { 8 int next_x = x + dir[k][0]; 9 int next_y = y + dir[k][1]; 10 11 if(next_x \u0026lt; 0 || next_x \u0026gt;= grid.size()) continue; 12 if(next_y \u0026lt; 0 || next_y \u0026gt;= grid[0].size()) continue; 13 if(grid[next_x][next_y] == \u0026#39;1\u0026#39;) 14 { 15 grid[next_x][next_y] = \u0026#39;0\u0026#39;; 16 DFS(grid, next_x, next_y); 17 } 18 } 19 } 20 21 int numIslands(vector\u0026lt;vector\u0026lt;char\u0026gt;\u0026gt;\u0026amp; grid) 22 { 23 int m = grid.size(); // row 24 int n = grid[0].size(); // column 25 int res = 0; 26 27 for(int i = 0; i \u0026lt; m; i++) 28 { 29 for(int j = 0; j \u0026lt; n; j++) 30 { 31 if(grid[i][j] == \u0026#39;1\u0026#39;) 32 { 33 res++; 34 grid[i][j] = \u0026#39;0\u0026#39;; 35 DFS(grid, i, j); 36 } 37 } 38 } 39 return res; 40 } 41private : 42 int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; 43}; 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/graph/medium/number-of-islands/","summary":"岛屿数量的解题思路与 C++ 实现。","title":"岛屿数量"},{"content":"题目：反转链表\n第一种解法：双指针\n思路如下： 原链表:1→2→3→4(pre=null) 1: null←1 2→3→4(pre=1) 2: null←1←2 3→4(pre=2) 3: null←1←2←3 4(pre=3) 4: null←1←2←3←4(pre=4)\n循环终止的条件是：当前节点为nullptr，之后因为在上一次循环中已经指定pre等于上一次循环的curr，因此直接返回pre即可\n1ListNode* reverseList(ListNode* head) 2 { 3 ListNode *pre = nullptr; 4 ListNode *curr = head; 5 while(curr) 6 { 7 ListNode* temp = curr-\u0026gt;next; 8 curr-\u0026gt;next = pre; 9 pre = curr; 10 curr = temp; 11 } 12 return pre; 13 } 第二种解法：递归\n总体思路还是在双指针的基础上进行的\nreverse函数接收当前节点与上一个节点，如果curr为空，也就是递归到了最后一个节点，就直接返回prev；\n若未到达尾节点，就跟双指针解法一样curr和pre都向后移一位，并再次调用reverse（注：此时temp和curr就是移动过后的curr和prev）\n在reverseList函数中对reverse的调用也是直接省去了curr和prev的初始化环节\n1ListNode* reverse(ListNode* curr, ListNode* prev) 2 { 3 if(curr == nullptr) return prev; 4 ListNode* temp = curr-\u0026gt;next; 5 curr-\u0026gt;next = prev; 6 return reverse(temp, curr); 7 } 8 9 ListNode* reverseList(ListNode* head) 10 { 11 return reverse(head, nullptr); 12 } ","permalink":"https://jiat-t.github.io/leetcode/list/easy/reverse-linked-list/","summary":"反转链表的解题思路与 C++ 实现。","title":"反转链表"},{"content":"题目：长度最小的子数组\n使用到的是滑动窗口\nminSubArrayLen（）\n1int minSubArrayLen(int target, vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int n = nums.size(), sum = 0, left = 0; 4 // 因为最后的结果输出的是子数组长度 5 // 所以最大长度肯定不会超过原数组长度 n 6 int res = n + 1; 7 // 遍历右边的节点 8 for(int right = 0; right \u0026lt; n; right++) 9 { 10 // 首先将右指针指向的节点加到 sum 中 11 sum += nums[right]; 12 // 不断缩小窗口 13 while(target \u0026lt;= sum - nums[left]) 14 { 15 sum -= nums[left]; 16 // 左端点右移 17 left++; 18 } 19 // 此时经过缩小之后，如果 sum 仍然大于等于 target 20 // 就记录一次答案 21 if(target \u0026lt;= sum) 22 { 23 res = std::min(res, right - left + 1); 24 } 25 } 26 return res == n + 1 ? 0 : res; 27} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/sliding-window/medium/minimum-size-subarray-sum/","summary":"长度最小的子数组的解题思路与 C++ 实现。","title":"长度最小的子数组"},{"content":"题目：打家劫舍 II\n核心是将“环”拆解为“两个线性表”\n对于环形的 n 间房屋，可以将其拆解为两个场景：\n场景 A：不偷最后一间房。此时可以偷的范围是 nums[0] 到 nums[n-2] 场景 B：不偷第一间房。此时可以偷的范围是 nums[1] 到 nums[n-1] 最后的结果一定是两者中的更大值\nrob ()\n1// 这里的辅助函数与上题一样 2int robRange(std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int start, int end) 3{ 4 int prev1 = 0, prev2 = 0; 5 for(int i = start; i \u0026lt; end; i++) 6 { 7 int temp = prev2; 8 prev2 = std::max(prev2, prev1 + nums[i]); 9 prev1 = temp; 10 } 11 return prev2; 12} 13int rob(vector\u0026lt;int\u0026gt;\u0026amp; nums) 14{ 15 int n = nums.size(); 16 if(n == 1) return nums[0]; 17 if(n == 2) return std::max(nums[0], nums[1]); 18 19 int res1 = robRange(nums, 0, n - 1); 20 int res2 = robRange(nums, 1, n); 21 22 return std::max(res1, res2); 23} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/house-robber-ii/","summary":"打家劫舍 II的解题思路与 C++ 实现。","title":"打家劫舍 II"},{"content":"题目：数组中的第K个最大元素\n解法一：优先队列（堆）\n时间复杂度：O(n log k)\t空间复杂度：O(k) 手动维护一个大小为 k 的最小堆（堆顶是堆中的最小元素），每当堆的大小超过 k 时，就将堆顶的元素弹出，这样最终留下来的堆顶元素就是 第k大的元素\n1int findKthLargest(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 2{ 3 std::priority_queue\u0026lt;int, std::vector\u0026lt;int\u0026gt;, greater\u0026lt;int\u0026gt;\u0026gt; pq; 4 for(const int\u0026amp; num : nums) 5 { 6 pq.emplace(num); 7 if(k \u0026lt; pq.size()) 8 pq.pop(); 9 } 10 return pq.top(); 11} 解法二：快速选择\n平均时间复杂度是 O(n)\n常规情况下的快排的平均时间复杂度是nlogn，所以需要手写一个快排\n具体实现：\n定义三个数组，分别存储大于、等于、小于基准值的数，通过一轮循环将其填充\n之后的判断存在三种情况：\n1.k 小于等于 big 数组的大小——也就是说，big数组中存在第一大、第二大\u0026hellip;..一直到第 k 大甚至更多元素，此时第 k 大的元素一定存在于 big 数组中，那么接下来就只需要在 big 数组里寻找即可\n2.k 大于原数组大小减去 small 数组大小——nums.size() - small.size() 代表的是 big 数组加上 equal 数组的大小，也就是说此时 big 与 equal 数组中的元素数量加起来都不足 k 个，那么第 k 大的元素就一定在 small数组里了；注意 quickSeclet 的第二个参数从 k 变成了 k - nums.size() + small.size()，这是因为，相对于整个数组，需要寻找的是第 k 大的元素，而相对于 small 数组来说，这个元素是第\nk - （nums.size() - small.size()）大的元素\n3.第 k 大位于 equal里面\n此时直接返回基准值即可\n1int findKthLargest(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 2{ 3 return quickSeclet(nums, k); 4} 5 6int quickSeclet(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 7{ 8 std::vector\u0026lt;int\u0026gt; big, equal, small; 9 int pivot = nums[std::rand() % nums.size()]; 10 for(int num : nums) 11 { 12 if(num \u0026lt; pivot) small.push_back(num); 13 else if(pivot \u0026lt; num) big.push_back(num); 14 else equal.push_back(num); 15 } 16 17 if(k \u0026lt;= big.size()) 18 return quickSeclet(big, k); 19 if(nums.size() - small.size() \u0026lt; k) 20 return quickSeclet(small, k - nums.size() + small.size()); 21 return pivot; 22} ","permalink":"https://jiat-t.github.io/leetcode/heap/medium/kth-largest-element-in-an-array/","summary":"数组中的第K个最大元素的解题思路与 C++ 实现。","title":"数组中的第K个最大元素"},{"content":"题目：翻转二叉树\n以前序遍历为例，从根节点开始，逐步交换左右子树（先交换左子树，后交换右子树），直到所有叶子节点都交换完毕；后序遍历同理，先把左子树的所有节点交换完毕，再处理右子树，最后在root节点处将整个左右子树交换\n而中序遍历是个特例，它会先交换左子树的所有节点，然后逐层返回到最顶部的root节点，将左子树和右子树交换，此时如果按照之前的逻辑，继续对右子树进行交换的话，相当于是对一开始的左子树又进行了一次交换，所有此时需要改变一下代码，进行对左子树）也就是一开始的右子树）进行交换，这样才能得到正确的结果\n1// 前序 2TreeNode* invertTree(TreeNode* root) 3{ 4 if(root == nullptr) return nullptr; 5 std::swap(root-\u0026gt;left, root-\u0026gt;right); 6 invertTree(root-\u0026gt;left); 7 invertTree(root-\u0026gt;right); 8 9 return root; 10} 11 12// 中序 13TreeNode* invertTree(TreeNode* root) 14{ 15 if(root == nullptr) return nullptr; 16 invertTree(root-\u0026gt;left); 17 std::swap(root-\u0026gt;left, root-\u0026gt;right); 18 invertTree(root-\u0026gt;left); 19 20 return root; 21} 22 23// 后序 24TreeNode* invertTree(TreeNode* root) 25{ 26 if(root == nullptr) return nullptr; 27 invertTree(root-\u0026gt;left); 28 invertTree(root-\u0026gt;right); 29 std::swap(root-\u0026gt;left, root-\u0026gt;right); 30 31 return root; 32} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/invert-binary-tree/","summary":"翻转二叉树的解题思路与 C++ 实现。","title":"翻转二叉树"},{"content":"题目：基本计算器 II\nhttps://leetcode.cn/problems/basic-calculator-ii/solutions/648647/ji-ben-ji-suan-qi-ii-by-leetcode-solutio-cm28/\n1int calculate(string s) 2{ 3 // 对于字符串中的第一个数字默认符号是\u0026#39;+\u0026#39; 4 char preSign = \u0026#39;+\u0026#39;; 5 // 维护一个栈用于存储数字 6 std::vector\u0026lt;int\u0026gt; st; 7 int num = 0, n = s.length(); 8 9 for(int i = 0 ; i \u0026lt; n; i++) 10 { 11 // 如果是数字，就进行累加 12 if(isdigit(s[i])) 13 { 14 num = num * 10 + (s[i] - \u0026#39;0\u0026#39;); 15 } 16 // 如果不是数字，同时也不是空格 17 if(!isdigit(s[i]) \u0026amp;\u0026amp; s[i] != \u0026#39; \u0026#39; || i == n - 1) 18 { 19 // 根据符号对之前累加得到的 num 进行处理 20 switch(preSign) 21 { 22 case \u0026#39;+\u0026#39; : 23 st.push_back(num); 24 break; 25 // 如果是减号，就将负数形式保存进栈中 26 case \u0026#39;-\u0026#39; : 27 st.push_back(-num); 28 break; 29 // 如果是乘除，就直接对栈顶的数字与当前保存的 num 进行对应运算 30 case \u0026#39;*\u0026#39; : 31 st.back() *= num; 32 break; 33 default : 34 st.back() /= num; 35 } 36 // 将 num 置为零，不用存入栈中，因为已经与栈顶元素进行过处理了 37 num = 0; 38 // 把当前符号传递给下一个数字使用 39 preSign = s[i]; 40 } 41 } 42 // 最后栈里只剩下被处理过正负号的数字，全部加起来就是结果 43 return accumulate(st.begin(), st.end(), 0); 44} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/stack/medium/basic-calculator-ii/","summary":"基本计算器 II的解题思路与 C++ 实现。","title":"基本计算器 II"},{"content":"题目：二叉搜索树中第 K 小的元素\n第一次的解法是通过中序遍历将树存入vector，再通过下标k直接访问元素，但是超时了\u0026hellip;\u0026hellip;应该是vector每次扩容的开销太大了\n下面直接分析代码：\n对于这段程序，只需要执行到第k个元素就可以停止了，后面的没必要去遍历，所以这里通过k来决定退出条件。\n首先遍历左子树（这里使用的还是中序遍历）， 一直到叶子节点的下一个空节点，如果在这个过程中，k已经被减为了零，那么当前节点就是我们需要找的节点；反之，如果左子树已经遍历完毕，而k不为零，就回到父节点，再在父节点处执行一次\u0026ndash;k的判断。最后，如果根节点和它的左子树的遍历完毕后，k还是没有为零，就开始遍历整颗右子树\n1int kthSmallest(TreeNode* root, int\u0026amp; k) 2{ 3 if(!root) return -1; 4 5 int res_left = kthSmallest(root-\u0026gt;left, k); 6 if(res_left != -1) return res_left; 7 8 if(--k == 0) return root-\u0026gt;val; 9 10 return kthSmallest(root-\u0026gt;right, k); 11} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/kth-smallest-element-in-a-bst/","summary":"二叉搜索树中第 K 小的元素的解题思路与 C++ 实现。","title":"二叉搜索树中第 K 小的元素"},{"content":"题目：用栈实现队列\n解法一：\n定义两个栈，一个正序记录输入的数字，另一个则倒序记录\n对于 push，每次压入元素时，需要重新记录 sk2；\n对于 pop，每次弹出元素时，需要重新记录 sk1\nMyQueue\n1class MyQueue 2{ 3public: 4 std::stack\u0026lt;int\u0026gt; sk1; 5 std::stack\u0026lt;int\u0026gt; sk2; 6 MyQueue() 7 { 8 } 9 10 void push(int x) 11 { 12 sk1.push(x); 13 std::stack\u0026lt;int\u0026gt; temp_sk1 = sk1; 14 std::stack\u0026lt;int\u0026gt; temp_sk2; 15 for(int i = 0; i \u0026lt; sk1.size(); i++) 16 { 17 temp_sk2.push(temp_sk1.top()); 18 temp_sk1.pop(); 19 } 20 sk2 = std::move(temp_sk2); 21 } 22 23 int pop() 24 { 25 int res = sk2.top(); 26 sk2.pop(); 27 std::stack\u0026lt;int\u0026gt; temp_sk1, temp_sk2 = sk2; 28 for(int i = 0; i \u0026lt; sk2.size(); i++) 29 { 30 temp_sk1.push(temp_sk2.top()); 31 temp_sk2.pop(); 32 } 33 sk1 = std::move(temp_sk1); 34 return res; 35 } 36 37 int peek() 38 { 39 return sk2.top(); 40 } 41 42 bool empty() 43 { 44 return sk1.empty(); 45 } 46}; 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/stack/easy/implement-queue-using-stacks/","summary":"用栈实现队列的解题思路与 C++ 实现。","title":"用栈实现队列"},{"content":"题目：回文链表\n使用的是双指针，以及上面那一题的反转函数\n一种比较粗暴的方法是将链表转换成数组，之后再使用双指针从两边向中间遍历比较，但是这个方法的空间复杂度是O(n)\n下面这个方法的思路是：找出链表的中点，翻转后半部分，再指定两个指针，分别从首节点与中间节点的后一个节点开始，向后进行遍历比较\n那么问题就变成了如何找到链表的中点：\n首先定义两个指针：slow指针和fast指针\nslow初始指向head，fast初始指向head-\u0026gt;next，之后进行循环，slow每次向后移动一位，fast则移动两位，直到fast的下一个节点为空（偶数链表）或fast本身为空（奇数链表），此时可以判断\u0026mdash;-slow指向的节点就是前半部分链表的最后一个节点\n之后对slow后面的另一半链表进行翻转，再逐个比较即可\n1ListNode* reverse(ListNode* curr, ListNode* prev) 2 { 3 if(curr == nullptr) return prev; 4 ListNode* temp = curr-\u0026gt;next; 5 curr-\u0026gt;next = prev; 6 return reverse(temp, curr); 7 } 8 9 bool isPalindrome(ListNode* head) 10 { 11 ListNode* slow = head; 12 ListNode* fast = head-\u0026gt;next; 13 int half = 1; 14 15 while(fast \u0026amp;\u0026amp; fast-\u0026gt;next) 16 { 17 slow = slow-\u0026gt;next; 18 fast = fast-\u0026gt;next-\u0026gt;next; 19 half++; 20 } 21 22 slow = reverse(slow, nullptr); 23 for(int i = 0; i \u0026lt; half; i++) 24 { 25 if(head-\u0026gt;val != slow-\u0026gt;val) return false; 26 head = head-\u0026gt;next; 27 slow = slow-\u0026gt;next; 28 } 29 return true; 30 } ","permalink":"https://jiat-t.github.io/leetcode/list/easy/palindrome-linked-list/","summary":"回文链表的解题思路与 C++ 实现。","title":"回文链表"},{"content":"题目：二叉树的最近公共祖先\n思路：\n因为需要从下往上进行遍历，所以采用的是后序遍历（左右中），这样就可以让中间节点拿到左右子树的信息，从而判断自身是否为公共祖先——如果子树没有搜索到目标节点，会返回空指针；只有当左右子树的返回值都不为空时，才可以判断当前节点为公共祖先\n具体实现：\n首先定义递归结束的条件——当前节点为空，或当前节点就是需要进行查找的节点\n之后就开始对左右子树进行递归遍历，当目标节点存在于左右子树中时，就直接返回当前节点（理由如上）；当只有左子树找到目标节点而右子树没有搜索到时，就返回左节点（因为此时两个目标值都位于左子树中）；只有右子树搜索到时同理\n1TreeNode* traversal(TreeNode* root, TreeNode* p, TreeNode* q) 2{ 3 if(!root) return nullptr; 4 if(root == p || root == q) return root; 5 6 TreeNode* left = traversal(root-\u0026gt;left, p, q); 7 TreeNode* right = traversal(root-\u0026gt;right, p, q); 8 9 if(left \u0026amp;\u0026amp; right) return root; 10 else if(left \u0026amp;\u0026amp; !right) return left; 11 else if(!left \u0026amp;\u0026amp; right) return right; 12 else return nullptr; 13} 14 15TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 16{ 17 return traversal(root, p, q); 18} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/lowest-common-ancestor-of-a-binary-tree/","summary":"二叉树的最近公共祖先的解题思路与 C++ 实现。","title":"二叉树的最近公共祖先"},{"content":"题目：除了自身以外数组的乘积\n思路如下：\n因为不能使用除法——通过一次遍历计算整个数组的乘积，之后再进行一次遍历，每次除以当前元素的值\n同时要求时间复杂度是O(n)，空间复杂度要求是O(1)，但是输出的数组不计入额外空间，所以需要直接对输出数组进行操作；\n可以先通过一次遍历计算出左边的乘积，再从后往前进行一次遍历，将右边的乘积再乘到res的当前元素上去，这样最终得到的res数组就是除自身外的元素的乘积\n注：这里需要提前预留好res的空间，而不是在循环中每次都调用push_back\nproductExceptSelf（）\n1vector\u0026lt;int\u0026gt; productExceptSelf(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 const int sz = nums.size(); 4 std::vector\u0026lt;int\u0026gt; res(sz); 5 res[0] = 1; 6 7 int temp = 1; 8 for(int i = 0; i \u0026lt; sz; i++) 9 { 10 res[i] = temp; 11 temp *= nums[i]; 12 } 13 14 temp = 1; 15 for(int j = sz - 1; 0 \u0026lt;= j; j--) 16 { 17 res[j] *= temp; 18 temp *= nums[j]; 19 } 20 21 return res; 22} 原文：medium\n","permalink":"https://jiat-t.github.io/leetcode/array/medium/product-of-array-except-self/","summary":"除了自身以外数组的乘积的解题思路与 C++ 实现。","title":"除了自身以外数组的乘积"},{"content":"题目：滑动窗口最大值\nmaxSlidingWindow（）\n1vector\u0026lt;int\u0026gt; maxSlidingWindow(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 2{ 3 int n = nums.size(); 4 std::vector\u0026lt;int\u0026gt; res(n - k + 1); 5 std::deque\u0026lt;int\u0026gt; dq; 6 7 for(int i = 0; i \u0026lt; n; i++) 8 { 9 while(!dq.empty() \u0026amp;\u0026amp; nums[dq.back()] \u0026lt;= nums[i]) 10 { 11 dq.pop_back(); 12 } 13 dq.push_back(i); 14 15 int left = i - k + 1; 16 if(dq.front() \u0026lt; left) 17 { 18 dq.pop_front(); 19 } 20 21 if(0 \u0026lt;= left) 22 { 23 res[left] = (nums[dq.front()]); 24 } 25 } 26 return res; 27} 原文：hard\n","permalink":"https://jiat-t.github.io/leetcode/sliding-window/hard/leetcode-sliding-window-hard/","summary":"滑动窗口最大值的解题思路与 C++ 实现。","title":"滑动窗口最大值"},{"content":"题目：搜索二维矩阵 II\n如图，不妨将矩阵旋转45度，将其视作一个图，此时它的形式类似于一颗_“ 二叉搜索树 ”_（左边节点都小于当前节点，右边节点都大于当前节点），所以可以采用在二叉搜索树中使用的查找算法\n具体实现：\n以右上方节点为根节点，如果目标值小于当前节点，就向左移动（列数减一）；如果目标值大于当前节点，就向右移动（行数加一）。如果直到最后，也就是超出行列的边界之后都没有找到对应值，就返回false，即矩阵中不存在目标值\n```cpp bool searchMatrix(vector\u0026lt;vector\u0026gt;\u0026amp; matrix, int target)\n{\nint rows = matrix.size(); int cols = matrix[0].size(); int row = 0, col = cols - 1; while(row \u0026lt; rows \u0026amp;\u0026amp; 0 \u0026lt;= col) { if(target \u0026lt; matrix[row][col]) col--; else if(matrix[row][col] \u0026lt; target) row++; else return true; } return false; }\n","permalink":"https://jiat-t.github.io/leetcode/matrix/medium/search-a-2d-matrix-ii/","summary":"搜索二维矩阵 II的解题思路与 C++ 实现。","title":"搜索二维矩阵 II"},{"content":"题目：完全平方数\n定义了一个数组，用来存储n之前每一个元素的完全平方数的最少数量，这样就得到了状态转移方程：\n$ dp[i] = 1 + \\min_{1 \\le j^2 \\le i} \\{ dp[i - j^2] \\} $\n其中，i 是从1到 n 的所有数字，j^2是小于等于 i 的所有完全平方数\n每次当 n 减去 i 之后，都会以 i 为右边界进行一次遍历，此时就是对每一个 i 进行”局部穷举“，看看此时有哪些组成 j^2 的最少数字数量，我们会选取最小的作为当前的结果\n1int numSquares(int n) 2{ 3 std::vector\u0026lt;int\u0026gt; f(n + 1); 4 for(int i = 1; i \u0026lt;= n; i++) 5 { 6 int minn = INT_MAX; 7 for(int j = 1; j * j \u0026lt;= i; j++) 8 minn = std::min(minn, f[i - j * j]); 9 10 f[i] = minn + 1; 11 } 12 return f[n]; 13} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/perfect-squares/","summary":"完全平方数的解题思路与 C++ 实现。","title":"完全平方数"},{"content":"题目：移动零\n思路：使用两个指针，一个从前往后对数组进行遍历，另一个始终指向前端的非零元素；只要指针 i 指向的元素非零，就将指针 k 当前指向的元素赋值为 num[i] ，最后再将指针 k 后面的元素全部置为零即可\nmoveZeroes（）\n1void moveZeroes(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int k = 0; 4 for(int i = 0; i \u0026lt; nums.size(); i++) 5 { 6 if(nums[i] != 0) 7 { 8 nums[k] = nums[i]; 9 k++; 10 } 11 } 12 for(int j = k; j \u0026lt; nums.size(); j++) 13 { 14 nums[j] = 0; 15 } 16} 原文：easy\n","permalink":"https://jiat-t.github.io/leetcode/double-pointers/easy/move-zeroes/","summary":"移动零的解题思路与 C++ 实现。","title":"移动零"},{"content":"题目：寻找重复数\n用到了**”环形链表”**的思想\n对于含有唯一 一个重复元素的数组，使用快慢指针进行遍历，fast每次两步（nums[nums[fast]]），slow每次一步（nums[slow]），那么之后一定会产生一个环，而入口就是那个重复的元素\n之后只需要再指定一个从起点出发的指针，与slow一起再次进行遍历，最终相遇的位置就是重复数字的位置\n（关于环形更具体的解释参见“链表 -\u0026gt; Medium”）\n1int findDuplicate(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int fast = nums[nums[0]], slow = nums[0]; 4 while(fast != slow) 5 { 6 fast = nums[nums[fast]]; 7 slow = nums[slow]; 8 } 9 10 int finder = 0; 11 while(finder != slow) 12 { 13 finder = nums[finder]; 14 slow = nums[slow]; 15 } 16 return slow; 17} ","permalink":"https://jiat-t.github.io/leetcode/technique/medium/find-the-duplicate-number/","summary":"寻找重复数的解题思路与 C++ 实现。","title":"寻找重复数"},{"content":"题目：最长递增子序列\n解法一：动态规划\n状态定义：dp[ i ]指的是“以第 i 个元素结尾的子序列的最大长度”\n状态转移：dp[ i ] = max( dp[ i ], dp[ j ] + 1 )，其中 j 是不大于 i 的正整数；\n方程的意义是——假设正在处理 nums[i]，我们想把它接在一个现有的子序列后面。\n为了保证“严格递增”，必须满足两个条件：\n1. **位置在前后**：那个数必须在 i 之前（即 j \u0026lt; i）。 2. **数值从小到大**：那个数必须比 `nums[i]` 小（即 nums[j] \u0026lt; nums[i]）。 1int lengthOfLIS(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 if(nums.empty()) return 0; 4 std::vector\u0026lt;int\u0026gt; dp(nums.size(), 1); 5 int res = 1; 6 for(int i = 0; i \u0026lt; nums.size(); i++) 7 { 8 for(int j = 0; j \u0026lt; i; j++) 9 { 10 if(nums[j] \u0026lt; nums[i]) 11 { 12 dp[i] = std::max(dp[i], dp[j] + 1); 13 } 14 } 15 res = std::max(res, dp[i]); 16 } 17 return res; 18} 补充解法：二分查找维护最小末尾\n维护一个数组 dp，dp【i】的含义是：长度为 i + 1 的子数组的末尾元素的最小值\n然后遍历数组 nums，在 dp 中通过二分查找，看看 num 的位置在哪里\n如果不存在任何一个数大于等于 num，也就是 it = num.end（），就将 num 接入 dp 的末尾，意味着子序列的长度可以加一如果当前数组中已经有一个数大于等于 num 了，就将这个数替换为 num（降低末尾元素的值，确保后续可以有更多元素加入） 时间复杂度：O(n logn)，其中 n 为 nums 的长度 空间复杂度：O(n) 1int lengthOfLIS(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 std::vector\u0026lt;int\u0026gt; dp; 4 5 for(int num : nums) 6 { 7 auto it = std::lower_bound(dp.begin(), dp.end(), num); 8 if(it == dp.end()) 9 { 10 dp.push_back(num); 11 } 12 else 13 { 14 *it = num; 15 } 16 } 17 return dp.size(); 18} 来源：语雀原笔记\n补充解法：原地修改\n直接对传入的 nums 数组进行修改，避免再创建一个新数组\nlengthOfLIS（）\n1int lengthOfLIS(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 // 从第一个元素开始 4 auto end = nums.begin(); 5 6 for(int num : nums) 7 { 8 // 在区间内找到第一个大于等于 num 的位置 9 auto it = lower_bound(nums.begin(), end, num); 10 11 // 将原来 g[j] 的值（一个较大的数）替换成更小的 num 12 // 为什么直接认为 num 一定小于之前的末尾元素？ 13 /* 因为 lower_bound(first, last, x) 返回的是第一个满足 元素 \u0026gt;= x 的位置。 14 因此，当 it != end 时，*it \u0026gt;= x 恒成立。 15 于是执行 *it = x 后： 16 如果 *it \u0026gt; x，新值变小（这正是我们想要的，因为要降低该长度的最小末尾）。 17 如果 *it == x，新值不变（没有影响）。 18 没有可能出现 *it \u0026lt; x 的情况，所以不存在“当前元素大于原末尾”的问题*/ 19 *it = num; 20 21 if(it == end) 22 { 23 // 此时将长度加一 24 end++; 25 } 26 } 27 return end - nums.begin(); 28} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/longest-increasing-subsequence/","summary":"最长递增子序列的解题思路与 C++ 实现。","title":"最长递增子序列"},{"content":"题目：零钱兑换\n确定状态：dp[ i ]代表 为了凑出 i 元，所需要的硬币数量\n状态转移：dp[ i ] = min(dp[ i ], dp[ i - coin ] + 1)，即金额 i 的最优解，必然是由某个较小的金额 i - coin 转移而来的。\n1int coinChange(vector\u0026lt;int\u0026gt;\u0026amp; coins, int amount) 2{ 3 std::vector\u0026lt;int\u0026gt; dp(amount + 1, amount + 1); 4 dp[0] = 0; 5 6 for(int i = 1; i \u0026lt;= amount; i++) 7 { 8 for(auto coin :coins) 9 { 10 if(coin \u0026lt;= i) 11 dp[i] = std::min(dp[i], dp[i - coin] + 1); 12 } 13 } 14 return dp[amount] \u0026gt; amount ? -1 : dp[amount]; 15} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/coin-change/","summary":"零钱兑换的解题思路与 C++ 实现。","title":"零钱兑换"},{"content":"题目：前 K 个高频元素\n思路：先使用**哈希表将每个数字与出现频率对应，再通过频率大小在最小堆中筛选出前 k 个高频元素，最后将堆中的频率对应的数字存入数组**并输出\n唯一要注意的是 pq 中存储的元素，不能只存储频率，而是要将频率与对应的数字绑定，不然最后会导致只知道”前 k 大的频率“，而不是”前 k 个高频元素“；这里使用的是 pair，比较器只会比较频率的大小，这样我们就能找到频率所对应的元素，并将其存入 res 数组\n1vector\u0026lt;int\u0026gt; topKFrequent(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 2{ 3 std::unordered_map\u0026lt;int, int\u0026gt; um; 4 for(int num : nums) 5 { 6 um[num]++; 7 } 8 9 auto cmp = [](const std::pair\u0026lt;int, int\u0026gt;\u0026amp; a, const std::pair\u0026lt;int, int\u0026gt;\u0026amp; b) 10 { 11 return b.first \u0026lt; a.first; 12 }; 13 std::priority_queue\u0026lt;std::pair\u0026lt;int, int\u0026gt;, vector\u0026lt;std::pair\u0026lt;int, int\u0026gt;\u0026gt;, decltype(cmp)\u0026gt; pq; 14 for(const auto\u0026amp; pair : um) 15 { 16 pq.emplace(pair.second, pair.first); 17 if(k \u0026lt; pq.size()) 18 pq.pop(); 19 } 20 21 std::vector\u0026lt;int\u0026gt; res; 22 res.reserve(k); 23 while(!pq.empty()) 24 { 25 res.push_back(pq.top().second); 26 pq.pop(); 27 } 28 return res; 29} ","permalink":"https://jiat-t.github.io/leetcode/heap/medium/top-k-frequent-elements/","summary":"前 K 个高频元素的解题思路与 C++ 实现。","title":"前 K 个高频元素"},{"content":"题目：字符串解码\n使用到的是栈和递归\n关于为什么是遇到‘【’就继续入栈：这是为了处理前置的字母，可以将其直接拼接到res的尾部\n1string decodeString(string s) 2{ 3 std::stack\u0026lt;std::pair\u0026lt;string, int\u0026gt;\u0026gt; stk; 4 string res; 5 int k = 0; 6 for(char c : s) 7 { 8 if(isalpha(c)) res += c; 9 else if(isdigit(c)) k = k * 10 + (c - \u0026#39;0\u0026#39;); 10 else if(c == \u0026#39;[\u0026#39;) 11 { 12 stk.emplace(std::move(res), k); 13 k = 0; 14 } 15 else 16 { 17 auto [pre_res, pre_k] = stk.top(); 18 stk.pop(); 19 while(pre_k--) 20 { 21 pre_res += res; 22 } 23 res = std::move(pre_res); 24 } 25 } 26 return res; 27} ","permalink":"https://jiat-t.github.io/leetcode/stack/medium/decode-string/","summary":"字符串解码的解题思路与 C++ 实现。","title":"字符串解码"},{"content":"题目：最长回文串\n思路：对于出现次数为偶数个的字符，可以直接将其添加至回文字符串两端；对于出现次数为奇数个的字符，最多取其中的 （ct - 1）个添加至两端，最后还可以从奇数字符中拿出一个放在最中间\nlongestPalindrome（）\n1int longestPalindrome(string s) 2{ 3 if(s.size() \u0026lt;= 1) return s.size(); 4 5 // 统计字符出现次数 6 std::unordered_map\u0026lt;char, int\u0026gt; um; 7 for(char c : s) 8 { 9 um[c]++; 10 } 11 12 int length = 0; 13 bool has_odd = false; 14 for(const auto\u0026amp; [ch, cnt] : um) 15 { 16 // 利用 int 的向下取整性质 17 // 可以将奇偶的情况统一处理 18 length += (cnt / 2) * 2; 19 // 判断是否存在奇数出现次数 20 if(cnt % 2 == 1) 21 { 22 has_odd = true; 23 } 24 } 25 // 如果 s 中含有奇数出现次数的字符 26 // 就在中间再加上这个字符（长度加一） 27 return length + (has_odd ? 1 : 0); 28} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/hash/easy/longest-palindrome/","summary":"最长回文串的解题思路与 C++ 实现。","title":"最长回文串"},{"content":"题目：字符串相加\n从后往前对两个字符串进行处理\naddStrings（）\n1string addStrings(string num1, string num2) 2{ 3 // 进位数字 4 int carry = 0; 5 int n1 = num1.size() - 1, n2 = num2.size() - 1; 6 7 string res = \u0026#34;\u0026#34;; 8 // 这里的 carry 是为了处理最高位相加之后的进位情况 9 while(0 \u0026lt;= n1 || 0 \u0026lt;= n2 || carry) 10 { 11 // 只有还有剩余数字时才取值 12 int num1_c = 0 \u0026lt;= n1 ? num1[n1] - \u0026#39;0\u0026#39; : 0; 13 int num2_c = 0 \u0026lt;= n2 ? num2[n2] - \u0026#39;0\u0026#39; : 0; 14 int sum = num1_c + num2_c + carry; 15 carry = (sum) / 10; 16 res.push_back((sum % 10) + \u0026#39;0\u0026#39;); 17 n1--;n2--; 18 } 19 // 因为是从后往前处理所以最后需要进行一次翻转操作 20 reverse(res.begin(), res.end()); 21 return res; 22} 原文：easy\n","permalink":"https://jiat-t.github.io/leetcode/string/easy/leetcode-string-easy/","summary":"字符串相加的解题思路与 C++ 实现。","title":"字符串相加"},{"content":"题目：分割等和子集\n1bool canPartition(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int sum = reduce(nums.begin(), nums.end()); 4 if(sum % 2 != 0) return false; 5 sum /= 2; 6 7 int n = nums.size(); 8 std::vector\u0026lt;bool\u0026gt; dp(sum + 1, false); 9 dp[0] = true; 10 for(int num : nums) 11 { 12 for(int i = sum; num \u0026lt;= i; i--) 13 { 14 if(dp[i - num]) dp[i] = true; 15 } 16 if(dp[sum]) return true; 17 } 18 return dp[sum]; 19} ","permalink":"https://jiat-t.github.io/leetcode/dynamic-programming/medium/partition-equal-subset-sum/","summary":"分割等和子集的解题思路与 C++ 实现。","title":"分割等和子集"},{"content":"题目：路径总和 III\n使用到了深搜和层序遍历\n注：此处并非最优解，但是我不想再看其他解法了\u0026hellip;.\n更好的答案参见这里吧437. 路径总和 III - 力扣（LeetCode）\n思路：\n简单来说，就是通过层序遍历，从每一个节点出发，进行一次深度优先搜索，并在搜索过程中不断地将当前总和与目标和进行比较，若相等则将路径数量加一\n具体实现：\ndfs函数：从传入的节点出发，先将当前值加到当前总和上去，然后与目标值进行比较；接着递归深入左子树和右子树（需要传入当前总和），之后左右子树又会进行相同的判断\npathSum函数：定义队列，按照层序遍历的顺序每次访问一个节点，并调用dfs函数\n1int pathSum(TreeNode* root, int targetSum) 2{ 3 if(!root) return 0; 4 std::queue\u0026lt;TreeNode*\u0026gt; q; 5 q.push(root); 6 int total = 0; 7 while(!q.empty()) 8 { 9 TreeNode* curr = q.front(); 10 q.pop(); 11 total += dfs(curr, 0, targetSum); 12 13 if(curr) 14 { 15 q.push(curr-\u0026gt;left); 16 q.push(curr-\u0026gt;right); 17 } 18 } 19 return total; 20} 21int dfs(TreeNode* curr, long long curr_sum, int targetSum) 22{ 23 if(!curr) return 0; 24 curr_sum += curr-\u0026gt;val; 25 int count = 0; 26 if(curr_sum == targetSum) count++; 27 28 count += dfs(curr-\u0026gt;left, curr_sum, targetSum); 29 count += dfs(curr-\u0026gt;right, curr_sum, targetSum); 30 31 return count; 32} 补充解法：前缀和与哈希表\nhttps://leetcode.cn/problems/path-sum-iii/solutions/2784856/zuo-fa-he-560-ti-shi-yi-yang-de-pythonja-fmzo/?envType=study-plan-v2\u0026amp;envId=top-100-liked\n核心公式：\n当前前缀和 - 历史前缀和 = targetSum\n移项可得：\n历史前缀和 = 当前前缀和 - targetSum\n因此，只需要使用一个哈希表记录“从根节点到当前节点的路径上，以每个节点为终点的前缀和的出现次数”，然后通过判断当前前缀和减去targetSum的值是否存在于哈希表中：如果存在，就说明对应的历史前缀和的终点节点到当前节点的和就是 targetSum\npathSum（）\n1int pathSum(TreeNode* root, int targetSum) 2{ 3 std::unordered_map\u0026lt;long long, int\u0026gt; count = {{0, 1}}; 4 int res = 0; 5 6 auto dfs = [\u0026amp;](this auto\u0026amp;\u0026amp; dfs, TreeNode* root, long long s) 7 { 8 if(root == nullptr) return; 9 10 s += root-\u0026gt;val; 11 res += count[s - targetSum]; 12 13 count[s]++; 14 dfs(root-\u0026gt;left, s); 15 dfs(root-\u0026gt;right, s); 16 count[s]--; 17 }; 18 19 dfs(root, 0); 20 return res; 21} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/binary-tree/medium/path-sum-iii/","summary":"路径总和 III的解题思路与 C++ 实现。","title":"路径总和 III"},{"content":"题目：找到字符串中所有字母异位词\n思路：从左往右移动窗口，每次移动一格；移动的过程中不断对 left 与 right 指向的元素进行“是否存在于‘p’中“的判断——如果存在，那么对应字符的出现频率减一\u0026hellip;..直到最后整个哈希表清零，就可以认为这个窗口中的元素满足条件，将其存入res中\n1 2vector\u0026lt;int\u0026gt; findAnagrams(string s, string p) 3 4{ 5 6 int ns = s.size(), np = p.size(); 7 8 if(ns \u0026lt; np) return {}; 9 10 std::vector\u0026lt;int\u0026gt; res; 11 12 13 14 std::vector\u0026lt;int\u0026gt; count(26, 0); 15 16 for(auto c : p) count[c - \u0026#39;a\u0026#39;]++; 17 18 19 20 int left = 0, right = 0, need = np; 21 22 while(right \u0026lt; ns) 23 24 { 25 26 char c = s[right]; 27 28 // 扩大窗口 29 30 if(count[c - \u0026#39;a\u0026#39;] \u0026gt; 0) need--; 31 32 count[c - \u0026#39;a\u0026#39;]--; 33 34 right++; 35 36 37 38 // 缩小窗口 39 40 if(right - left \u0026gt; np) 41 42 { 43 44 char d = s[left]; 45 46 if(count[d - \u0026#39;a\u0026#39;] \u0026gt;= 0) need++; 47 48 count[d - \u0026#39;a\u0026#39;]++; 49 50 left++; 51 52 } 53 54 55 56 if(need == 0) res.push_back(left); 57 58 } 59 60 return res; 61 62} ","permalink":"https://jiat-t.github.io/leetcode/sliding-window/medium/find-all-anagrams-in-a-string/","summary":"找到字符串中所有字母异位词的解题思路与 C++ 实现。","title":"找到字符串中所有字母异位词"},{"content":"题目：二叉树的直径\n最开始的想法是：分别求左右子树的最大深度，之后再加上二就是二叉树的直径\n但是这个方法仅仅只是”局部最优解“而非”全局最优解“，因为二叉树的直径也可能不会跨越根节点（比如当左子树为深度非常大的满二叉树，而右子树仅仅只有一个节点时，那么直径就会在左子树出现），因此需要判断直径的计算是否会跨越根节点\n1int maxDepth(TreeNode* curr, int\u0026amp; max_dep) 2{ 3 if(!curr) return 0; 4 5 int left_dep = maxDepth(curr-\u0026gt;left, max_dep); 6 int right_dep = maxDepth(curr-\u0026gt;right, max_dep); 7 max_dep = std::max(max_dep, left_dep + right_dep); 8 return std::max(left_dep, right_dep) + 1; 9} 10 11int diameterOfBinaryTree(TreeNode* root) 12{ 13 int max_dep = 0; 14 maxDepth(root, max_dep); 15 return max_dep; 16} ","permalink":"https://jiat-t.github.io/leetcode/binary-tree/easy/diameter-of-binary-tree/","summary":"二叉树的直径的解题思路与 C++ 实现。","title":"二叉树的直径"},{"content":"题目：和为 K 的子数组\n解法一：暴力解法\n使用双层循环，每次从当前位置向后遍历，直到总和等于目标值（res加一）\nsubarraySum（）\n1int subarraySum(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 2{ 3 int n = nums.size(); 4 int res = 0; 5 for(int i = 0; i \u0026lt; n; i++) 6 { 7 int sum = 0; 8 for(int j = i; j \u0026lt; n; j++) 9 { 10 sum += nums[j]; 11 if(sum == k) 12 { 13 res++; 14 continue; 15 } 16 } 17 } 18 return res; 19} 解法二：前缀和\n思路：使用一张哈希表记录之前出现的和以及对应的次数，当前连续子序列长度为sum（从头一直加到尾），如果能够在第 j 个元素的位置，找到在前 j 个元素中，和为 sum - k 的连续子数组，那么从 j 到 i 的这段子数组的和就一定等于 k （k = sum - （sum - k））\n更简单的理解是：使用哈希表记录每个元素的前缀和，并统计所有相同前缀和的出现次数；当我们遍历到第 j 个元素时，此时需要查找在【0，j】范围内，是否存在子数组之和为 k——等价于在【0，j - 1】范围内，是否存在子数组之和为 sum - k，因为我们已经直到以 j 为结尾的前缀和为 sum，只需要减去 （sum - k），所得到的子数组之和就为 k\nsubarraySum（）\n1int subarraySum(vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) 2{ 3 std::unordered_map\u0026lt;int, int\u0026gt; prefixCount; 4 prefixCount[0] = 1; 5 6 int res = 0, sum = 0; 7 for(auto num : nums) 8 { 9 sum += num; 10 if(prefixCount.count(sum - k)) 11 { 12 res += prefixCount[sum - k]; 13 } 14 prefixCount[sum]++; 15 } 16 return res; 17} 原文：medium\n","permalink":"https://jiat-t.github.io/leetcode/substring/medium/leetcode-substring-medium/","summary":"和为 K 的子数组的解题思路与 C++ 实现。","title":"和为 K 的子数组"},{"content":"题目：每日温度\n使用到的是单调栈\n首先想到的是双重循环遍历😅\u0026hellip;\u0026hellip;..但这会导致n方的复杂度，所以这个方法不好\n我们想要的是，通过一次遍历就可以找到所有符合条件的结果，这就意味着需要使用一种数据结构来存储遍历过的元素，再在之后的循环中进行比较\n单调栈就是指，在入栈过程中，通过不断剔除不符合条件的元素，以保持栈内的元素按照某种单调性排列，即有序性，这与此题的要求高度吻合\n所以在代码中，我们定义了一个stack，用于存储需要进行比较的数组的下标（至于为什么不存元素，这是因为后面我们需要用这个下标找到输出结果中对应的位置，并且距离计算使用的也是下标）\n具体的运作原理是：栈顶元素是上一次循环留下来的结果，将栈顶元素与当前元素进行比较，如果当前元素更大，就认为栈顶元素找到了第一个更高温度，此时让栈顶元素出栈，因为他已经比较完毕了；由于while循环，若此时栈不为空，那么就会再次进行比较，如果当前元素更小，就入栈，这也就是选择单调栈的原因（保持了大小的有序性）\n注意：这里可以进行优化，主要是把多次调用的函数提前保存，避免调用函数的开销（因为这是在循环中，所以开销会很大）\n1// 2// 优化前（33ms） 3// 4vector\u0026lt;int\u0026gt; dailyTemperatures(vector\u0026lt;int\u0026gt;\u0026amp; temperatures) 5{ 6 std::stack\u0026lt;int\u0026gt; st; 7 st.push(0); 8 int sz = temperatures.size(); 9 std::vector\u0026lt;int\u0026gt; res(sz, 0); 10 11 for(int j = 1; j \u0026lt; sz; j++) 12 { 13 while(!st.empty() \u0026amp;\u0026amp; temperatures[st.top()] \u0026lt; temperatures[j]) 14 { 15 res[st.top()] = j - st.top(); 16 st.pop(); 17 } 18 st.push(j); 19 } 20 return res; 21} 22 23// 24// 优化后（28ms） 25// 26vector\u0026lt;int\u0026gt; dailyTemperatures(vector\u0026lt;int\u0026gt;\u0026amp; temperatures) 27{ 28 std::stack\u0026lt;int\u0026gt; st; 29 int sz = temperatures.size(); 30 std::vector\u0026lt;int\u0026gt; res(sz, 0); 31 32 for(int j = 0; j \u0026lt; sz; j++) 33 { 34 int t = temperatures[j]; 35 while(!st.empty() \u0026amp;\u0026amp; temperatures[st.top()] \u0026lt; t) 36 { 37 int k = st.top(); 38 res[k] = j - k; 39 st.pop(); 40 } 41 st.push(j); 42 } 43 return res; 44} ","permalink":"https://jiat-t.github.io/leetcode/stack/medium/daily-temperatures/","summary":"每日温度的解题思路与 C++ 实现。","title":"每日温度"},{"content":"题目：划分字母区间\n思路：\n记录每个字母最后出现的位置，从第一个元素开始不断进行划分，（除第一次外）每一次的起始位置都是上一次结束位置的下一个位置\n之所以这么做，是因为每个相同的字母都必须出现在同一区间，也就是说划分出来的子区间至少要包含这个字母第一次出现到最后一次出现的区间；同时区间也可能出现覆盖的情况（比如 a 的区间是 [0, 8]，而 b 的区间是 [1, 7]，那么此时就要以 a 的区间为最终划分的依据，这也就是为什么记录每个元素最后一次出现的位置的原因\n具体实现：\n首先使用一个数组得到每个字母各自的区间末端（注意不要使用哈希表或者map，这两个数据结构对链式结构很不友好，实测array的时间复杂度超过100%， map超过54%，unordered_map超过11%）\n之后再通过一个循环，不断对end进行更新，直到下标走到end的位置，此时也就意味着第一个区间被划分出来了，那么就更新start，从当前end的下一个下标开始新一个区间的划分\n1vector\u0026lt;int\u0026gt; partitionLabels(string s) 2{ 3 std::array\u0026lt;int, 26\u0026gt; lastPos; 4 for(int i = 0; i \u0026lt; s.size(); i++) 5 { 6 lastPos[s[i] - \u0026#39;a\u0026#39;] = i; 7 } 8 9 std::vector\u0026lt;int\u0026gt; res; 10 int start = 0, end = 0; 11 for(int i = 0; i \u0026lt; s.size(); i++) 12 { 13 end = std::max(end, lastPos[s[i] - \u0026#39;a\u0026#39;]); 14 if(i == end) 15 { 16 res.push_back(end - start + 1); 17 start = end + 1; 18 } 19 } 20 return res; 21} ","permalink":"https://jiat-t.github.io/leetcode/greedy-algorithm/medium/partition-labels/","summary":"划分字母区间的解题思路与 C++ 实现。","title":"划分字母区间"},{"content":"题目：三角形的最大周长\n先将数组进行排序，之后从后往前进行遍历，每次判断上一个元素与上上个元素之和是否大于当前元素（三角形两边之和大于第三边）\n如果条件满足，直接计算周长并返回；否则，进入下一次循环\nlargestPerimeter（）\n1int largestPerimeter(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 std::sort(nums.begin(), nums.end()); 4 int res = 1; 5 for(int i = nums.size() - 1; 2 \u0026lt;= i; --i) 6 { 7 if(nums[i - 1] + nums[i - 2] \u0026gt; nums[i]) 8 return nums[i] + nums[i - 1] + nums[i - 2]; 9 else 10 continue; 11 } 12 return 0; 13} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/greedy-algorithm/easy/largest-perimeter-triangle/","summary":"三角形的最大周长的解题思路与 C++ 实现。","title":"三角形的最大周长"},{"content":"题目：腐烂的橘子\n使用的是广度优先搜索\n参考了这个解法：\nhttps://leetcode.cn/problems/rotting-oranges/solutions/2773461/duo-yuan-bfsfu-ti-dan-pythonjavacgojsrus-yfmh/?envType=study-plan-v2\u0026amp;envId=top-100-liked\n腐烂的橘子 - 力扣（LeetCode）\n腐烂的橘子 - 在给定的 m x n 网格 grid 中，每个单元格可以有以下三个值之一： * 值 0 代表空单元格； * 值 1 代表新鲜橘子； * 值 2 代表腐烂的橘子。 每分钟，腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。 返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能，返回 -1 。 示例 1： [https://assets.leetcode.cn/aliyun-lc-upload/uploads/2019/02/16/oranges.png] 输入：grid = [[2,1,1],[1,1,0],[0,1,1]] 输出：4 示例 2： 输入：grid = [[2,1,1],[0,1,1],[1,0,1]] 输出：-1 解释：左下角的橘子（第 2 行， 第 0 列）永远不会腐烂，因为腐烂只会发生在 4 个方向上。 示例 3： 输入：grid = [[0,2]] 输出：0 解释：因为 0 分钟时已经没有新鲜橘子了，所以答案就是 0 。 提示： * m == grid.length * n == grid[i].length * 1 \u0026lt;= m, n \u0026lt;= 10 * grid[i][j] 仅为 0、1 或 2\norangesRotting（）\n1class Solution 2{ 3public: 4 int orangesRotting(vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; grid) 5 { 6 int row = grid.size(), column = grid[0].size(); 7 int time = 0, fresh = 0; 8 std::vector\u0026lt;std::pair\u0026lt;int, int\u0026gt;\u0026gt; oranges; 9 10 for(int j = 0; j \u0026lt; column; j++) 11 { 12 for(int i = 0; i \u0026lt; row; i++) 13 { 14 if(grid[i][j] == 2) 15 { 16 oranges.emplace_back(i, j); 17 } 18 else if(grid[i][j] == 1) 19 { 20 fresh++; 21 } 22 } 23 } 24 25 while(fresh \u0026amp;\u0026amp; !oranges.empty()) 26 { 27 time++; 28 std::vector\u0026lt;std::pair\u0026lt;int, int\u0026gt;\u0026gt; next; 29 for(auto\u0026amp; [x, y] : oranges) 30 { 31 for(auto d : dir) 32 { 33 int next_x = x + d[0], next_y = y + d[1]; 34 if(0 \u0026lt;= next_x \u0026amp;\u0026amp; next_x \u0026lt; row \u0026amp;\u0026amp; 0 \u0026lt;= next_y \u0026amp;\u0026amp; next_y \u0026lt; column \u0026amp;\u0026amp; grid[next_x][next_y] == 1) 35 { 36 fresh--; 37 grid[next_x][next_y] = 2; 38 next.emplace_back(next_x, next_y); 39 } 40 } 41 } 42 oranges = next; 43 } 44 return fresh ? -1 : time; 45 } 46private : 47 int dir[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}}; 48}; 原文：medium\n","permalink":"https://jiat-t.github.io/leetcode/graph/medium/rotting-oranges/","summary":"腐烂的橘子的解题思路与 C++ 实现。","title":"腐烂的橘子"},{"content":"题目：最长公共子序列\n解法一：二维数组\n将两个字符串分别作为行 列以矩阵形式表示，同时将第一行、第一列填充为0，表示空字符串\n之后从(1, 1)开始进行遍历（因为i和j不是从零开始，所以之后如果要使用对应下标的text1与text2，要将i、j减一），如果两个字符串的第i-1与j-1个字符相同，就在矩阵对应的位置填充“dp[i - 1][j - 1] + 1”，其中dp[i - 1][j - 1]代表着“text1的前 i 个字符与text2的前 j 个字符的最长公共子序列”\n如果不同，就回滚到上一个字符，取text1的前 i-1 个字符与text2的前 j 个字符 与 text1的前 i 个字符与text2的前 j - 1 个字符中的最大值\n1int longestCommonSubsequence(string text1, string text2) 2{ 3 int res = 0; 4 int n1 = text1.size(), n2 = text2.size(); 5 std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; dp(n1 + 1, std::vector\u0026lt;int\u0026gt;(n2 + 1, 0)); 6 7 for(int i = 1; i \u0026lt;= n1; i++) 8 { 9 for(int j = 1; j \u0026lt;= n2; j++) 10 { 11 if(text1[i - 1] == text2[j - 1]) 12 { 13 dp[i][j] = dp[i - 1][j - 1] + 1; 14 } 15 else 16 { 17 dp[i][j] = std::max(dp[i - 1][j], dp[i][j - 1]); 18 } 19 } 20 } 21 return dp[n1][n2]; 22} 解法二：一维滚动数组\n因为判断时只需要用到左，上，左上的三个值，所以 一维数组 + 一个额外变量 同样可以完成任务\ndp数组第一个元素同样是零，进入循环后，先将当前元素进行保存，以供下一次循环使用（这里之所以使用 i + 1 而不是 i ，是因为第一位已经被定义为了零，所以要从第二位开始），之后根据字符是否相同更新当前位置的值——如果相同，则在上一个元素的基础上加一（代表总长度加一）；如果不同，则取dp[i + 1]（舍弃text1的当前字符）与dp[i]（舍弃text2的当前字符）中的最大值\nt[0] t[1] t[2] s[0] ✓ ✓ ✓\ns[1] ✓ ? ✓\ns[2] ✓ ✓ ✓\n总之就是记住，如果想要求 ？这个格子的值，当两个字符相同时，直接将左上角的值 + 1 赋过来；如果不同，就取左边与上方元素的更大者\n1int longestCommonSubsequence(string text1, string text2) 2{ 3 int n1 = text1.size(), n2 = text2.size(); 4 std::vector\u0026lt;int\u0026gt; dp(n2 + 1); 5 6 for(auto c : text1) 7 { 8 for(int i = 0, pre = 0; i \u0026lt; n2; i++) 9 { 10 int temp = dp[i + 1]; 11 dp[i + 1] = (c == text2[i]) ? pre + 1 : std::max(dp[i + 1], dp[i]); 12 pre = temp; 13 } 14 } 15 return dp[n2]; 16} ","permalink":"https://jiat-t.github.io/leetcode/multidimensional-dynamic-programming/medium/longest-common-subsequence/","summary":"最长公共子序列的解题思路与 C++ 实现。","title":"最长公共子序列"},{"content":"题目：删掉一个元素以后全为 1 的最长子数组\n原笔记（代码待复核）\n维护一个滑动窗口，窗口内只能存在一个零，同时每一步都动态更新最长子数组的长度\n当窗口内零的数量大于一时，将左边界右移，直到零的数量变为一\nlongestSubarray（）\n1int longestSubarray(vector\u0026lt;int\u0026gt;\u0026amp; nums) 2{ 3 int left = 0, zero_count = 0, res = 0; 4 5 for(int right = 0; right \u0026lt; nums.size(); right++) 6 { 7 if(nums[right] == 0) 8 zero_count++; 9 10 while(1 \u0026lt; zero_count) 11 { 12 if(nums[left] == 0) 13 zero_count--; 14 15 left++; 16 } 17 18 res = std::max(right - left + 1 - 1,); 19 } 20 return res; 21} 来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/leetcode/sliding-window/medium/longest-subarray-of-1s-after-deleting-one-element/","summary":"删掉一个元素以后全为 1 的最长子数组的解题思路与 C++ 实现。","title":"删掉一个元素以后全为 1 的最长子数组"},{"content":"\nUE 的 GAS 是什么 GAS（Gameplay Ability System）是 UE 用于构建技能、属性、Buff/Debuff、状态控制和多人同步的一套玩法框架。它主要解决的不是“播放一个技能动画”，而是技能规模扩大以后出现的复杂问题：\n技能能否释放； 消耗与冷却； 动画、命中和伤害的时序； 眩晕、霸体、无敌等状态互斥； Buff叠层和持续时间； 客户端预测与服务器校验； 特效、音效和玩法逻辑分离。 可以把GAS概括为：\n1ASC负责统筹 2GA负责执行行为 3GE负责修改状态 4Attribute负责保存数值 5Tag负责描述和约束状态 6AbilityTask负责异步流程 7GameplayCue负责表现 一、GAS的核心组成 1. Ability System Component：系统中枢 UAbilitySystemComponent，简称 ASC，是Actor进入GAS体系的入口。\n它负责管理：\n当前拥有的技能； 技能的授予、激活和移除； Attribute和AttributeSet； 当前生效的GameplayEffect； 当前拥有的GameplayTag； GameplayEvent； 技能网络复制和预测。 一般让Actor实现IAbilitySystemInterface，通过GetAbilitySystemComponent()返回ASC。ASC既可以放在Character上，也可以放在PlayerState上。将ASC放在PlayerState上，可以让属性、技能和长时间冷却在角色死亡、换Pawn后继续存在。(Epic Games Developers)\nASC放在哪里 放在Character：\n实现简单； 适合AI、怪物、生命周期与角色一致的对象； Character销毁时，ASC及其状态通常也随之销毁。 放在PlayerState：\n适合玩家角色； Pawn死亡重生后仍可保留等级、技能和冷却； 需要处理PlayerState和新Pawn之间的初始化关系。 面试中经常问：\n玩家死亡并重新生成Character，技能冷却不应该重置，ASC应该放在哪里？\n通常回答：放在PlayerState，PlayerState作为长期Owner，当前Character作为技能实际操作的Avatar。\n2. Gameplay Ability：一个可执行的能力 UGameplayAbility，简称 GA，描述“角色要做什么”。\n例如：\n普通攻击； 翻滚； 冲刺； 格挡； 喝药； 火球术； 受击； 死亡； 被动技能。 GA不只是一个函数。它可以跨越多帧执行，等待动画、输入、命中数据或GameplayEvent，并在结束或取消时统一清理。它支持成本、冷却、标签约束、网络执行策略和客户端预测。(Epic Games Developers)\n一个Ability的基本生命周期 1服务器授予Ability 2 ↓ 3GiveAbility → FGameplayAbilitySpec 4 ↓ 5TryActivateAbility 6 ↓ 7CanActivateAbility 8 ↓ 9ActivateAbility 10 ↓ 11CommitAbility 12 ↓ 13启动AbilityTask并等待动画/输入/事件 14 ↓ 15造成效果或修改状态 16 ↓ 17EndAbility / CancelAbility GiveAbility通常只能由权威端执行，并返回FGameplayAbilitySpecHandle。TryActivateAbility会先检查能否激活，再真正调用激活逻辑；CommitAbility一般负责应用技能消耗和冷却；能力执行结束后必须调用EndAbility，否则GAS会继续认为技能处于激活状态，相关阻塞标签也可能一直存在。(Epic Games Developers)\nCommitAbility为什么重要 一般不要一进入ActivateAbility就无条件扣蓝、扣体力。\n更典型的流程是：\n1ActivateAbility 2→ 检查目标或开始瞄准 3→ 确定技能确实可以执行 4→ CommitAbility 5→ 扣除资源并进入冷却 6→ 执行后续技能流程 如果CommitAbility失败，通常应立即结束Ability。\n3. Attribute与AttributeSet：角色数值 Attribute是GAS管理的浮点型玩法数值，例如：\n1Health 2MaxHealth 3Mana 4Stamina 5AttackPower 6Defense 7MoveSpeed 8CriticalRate 这些属性通常放在继承自UAttributeSet的类中。当前官方文档要求AttributeSet及其Attribute在原生C++中定义。(Epic Games Developers)\nBase Value与Current Value FGameplayAttributeData包含两个重要值：\nBase Value：基础值或永久值； Current Value：考虑当前临时Modifier后的最终值。 例如：\n1基础移动速度：600 2减速Debuff：-30% 3Current MoveSpeed：420 减速结束后，Current恢复到600，而Base没有改变。AttributeSet将属性集中管理，并支持属性复制、临时修改和变化回调。(Epic Games Developers)\n常见回调 PreAttributeChange 属性变化前执行，常用于限制范围：\n1void UMyAttributeSet::PreAttributeChange( 2 const FGameplayAttribute\u0026amp; Attribute, 3 float\u0026amp; NewValue) 4{ 5 Super::PreAttributeChange(Attribute, NewValue); 6 7 if (Attribute == GetMoveSpeedAttribute()) 8 { 9 NewValue = FMath::Clamp(NewValue, 0.0f, 1200.0f); 10 } 11} 不建议在这里处理死亡、受击等复杂玩法反应。\nPostGameplayEffectExecute Instant GameplayEffect完成修改后执行，适合：\n把Health限制在0~MaxHealth； 处理Damage元属性； 触发死亡； 更新玩法状态。 官方也将PreAttributeChange定位为数值约束，将PostGameplayEffectExecute定位为效果执行后的最终处理和玩法反应。(Epic Games Developers)\n4. Gameplay Effect：改变角色状态 UGameplayEffect，简称 GE，负责描述“角色状态如何改变”。\n例如：\n造成100点伤害； 恢复50点生命； 增加20%攻击力，持续10秒； 每秒受到5点毒伤，持续8秒； 永久增加最大生命； 消耗30点体力； 添加眩晕状态； 添加技能冷却。 GameplayEffect本身通常是不可变的数据资产；运行时真正携带等级、来源、目标、动态数值和上下文的是FGameplayEffectSpec。(Epic Games Developers)\nGE的三种持续类型 类型\n用途\n示例\nInstant\n立即执行，不进入Active GE容器\n伤害、治疗、永久加点\nHas Duration\n持续一段时间\n10秒加速、5秒眩晕\nInfinite\n无限持续，直到主动移除\n装备加成、被动状态\n带持续时间的GE会进入ASC的Active Gameplay Effects Container；Instant GE执行后不会作为持续效果保存在其中。Periodic GE则会按设定周期重复执行。(Epic Games Developers)\nModifier、MMC与Execution Calculation Modifier 适合简单数值操作：\n1Health - 100 2AttackPower + 20 3MoveSpeed × 0.7 MMC UGameplayModMagnitudeCalculation用于计算一个Modifier的Magnitude。\n例如：\n1护盾值 = 技能等级 × 50 + 法术强度 × 0.6 它主要负责“算出一个数”，然后交给Modifier使用。(Epic Games Developers)\nExecution Calculation UGameplayEffectExecutionCalculation适合复杂计算，并且可以一次输出多个属性修改。\n例如伤害公式：\n1基础伤害 2→ 读取攻击者攻击力 3→ 读取目标防御力 4→ 计算暴击 5→ 计算元素抗性 6→ 计算最终Damage 7→ 同时产生生命削减、韧性削减和吸血 Execution可以读取来源和目标的属性快照，再向输出中添加多个Modifier。(Epic Games Developers)\n面试中经常问：\nMMC和Execution Calculation有什么区别？\n可以回答：\nMMC通常为一个Modifier计算Magnitude，更适合可复用的单值公式；Execution Calculation适合一次执行涉及来源属性、目标属性、多步判断和多个输出的复杂公式，例如伤害结算。\n5. Gameplay Tag：GAS的状态语言 GameplayTag是层级化标签，例如：\n1Ability.Attack.Light 2Ability.Attack.Heavy 3Ability.Movement.Dodge 4 5State.Attacking 6State.Stunned 7State.Dead 8State.Invincible 9 10Cooldown.Skill.Fireball 11Event.Melee.Hit 12GameplayCue.Hit.Fire Tag不是普通字符串，也不仅是Enum。它们可以形成层级关系：\n1State.CrowdControl.Stun 同时也匹配更上层的：\n1State.CrowdControl 2State GameplayTag可以用于描述对象类型、当前状态、事件和触发条件。(Epic Games Developers)\nTag在Ability中的典型用途 假设角色处于：\n1State.Stunned 翻滚Ability可以配置：\n1ActivationBlockedTags: 2 State.Stunned 3 State.Dead 攻击期间授予：\n1ActivationOwnedTags: 2 State.Attacking 重攻击启动时，可以配置：\n1CancelAbilitiesWithTag: 2 Ability.Attack.Light GAS原生支持使用Tag：\n阻止其他技能； 取消正在执行的技能； 要求角色必须具有某状态； 要求角色不得具有某状态； 判断目标是否可被技能命中。(Epic Games Developers) 这比散落在各处的布尔变量更适合复杂技能系统：\n1if (!bDead \u0026amp;\u0026amp; !bStunned \u0026amp;\u0026amp; !bAttacking \u0026amp;\u0026amp; !bClimbing \u0026amp;\u0026amp; ...) 会逐渐变成：\n1ActivationBlockedTags: 2 State.Dead 3 State.Stunned 4 State.Climbing 6. Ability Task：跨帧异步流程 GameplayAbility通常不依靠自己的Tick驱动流程，而是启动UAbilityTask。\n常见Task包括：\n播放Montage并等待； 等待GameplayEvent； 等待输入按下或释放； 等待延时； 等待目标数据； 等待属性变化； 移动角色到指定位置。 AbilityTask可以跨越多帧，通过Delegate或蓝图输出引脚通知Ability，并会在所属Ability结束时最迟终止。(Epic Games Developers)\n例如蓄力攻击：\n1ActivateAbility 2→ WaitInputRelease 3→ 玩家持续按住 4→ 计算蓄力时间 5→ 输入释放 6→ 播放攻击Montage 7→ 等待命中事件 8→ 应用伤害GE 9→ EndAbility AbilityTask和普通异步任务的区别 AbilityTask并不等同于“把工作放到其他CPU线程”。\n它主要是 玩法流程上的异步等待：\n等动画完成； 等网络目标数据； 等玩家输入； 等事件； 等时间。 它通常仍然参与游戏线程上的Ability流程。\n7. Gameplay Cue：只负责表现 GameplayCue用于表现性反馈：\nNiagara特效； 音效； 镜头震动； 材质变化； 命中特效； Buff持续特效。 常见生命周期：\nOnActive； WhileActive； Removed； Executed。 GameplayCue需要使用以GameplayCue开头的Tag，例如：\n1GameplayCue.Hit.Slash 2GameplayCue.Buff.Burning GameplayCue主要是网络友好的外观表现机制，不应承载扣血、添加状态等关键玩法逻辑。官方明确指出Cue不使用可靠复制，因此关键玩法结果不能依赖Cue是否成功播放。(Epic Games Developers)\n正确关系是：\n1GameplayEffect决定目标受到伤害 2GameplayCue负责播放受击火花和音效 二、用你的近战攻击项目理解GAS 假设把你现在UE5 RPG项目里的近战攻击改成GAS，可以设计成下面这样。\n1. 按下攻击键 Enhanced Input不直接调用“扣血函数”，而是让ASC尝试激活：\n1Ability.Attack.Light 2. GAS检查能否激活 检查：\n1角色是否死亡 2角色是否眩晕 3角色是否正在攀爬 4体力是否足够 5技能是否处于冷却 6当前是否允许进入下一段连击 这些条件主要由Tag、Cost GE、Cooldown GE和CanActivateAbility共同处理。\n3. 激活攻击Ability 1GA_LightAttack 2→ CommitAbility 3→ 应用体力消耗 4→ 应用攻击冷却 5→ 添加State.Attacking 6→ 播放Montage 4. 动画到达命中帧 Anim Notify发送GameplayEvent：\n1Event.Melee.Trace.Begin Ability等待该Event，然后开始武器Trace。\n5. 检测到目标 生成Effect Spec：\n1GE_Damage 其中保存：\n攻击者； 目标； 技能等级； 命中位置； 动态伤害； 暴击信息； Effect Context。 之后将Spec应用到目标ASC。\n6. 目标结算伤害 Execution Calculation读取：\n1攻击者AttackPower 2目标Defense 3技能倍率 4暴击率 5伤害类型 6目标抗性 算出最终Damage。\n目标AttributeSet在PostGameplayEffectExecute中：\n1Health -= Damage 2→ Clamp Health 3→ Health \u0026lt;= 0时进入死亡流程 7. 播放表现 触发：\n1GameplayCue.Hit.Slash 播放粒子、声音、受击闪白，但不在Cue里执行扣血。\n8. 攻击完成或被打断 1Montage完成 2→ EndAbility 3 4收到State.Stunned 5→ CancelAbility 6→ 停止Montage 7→ 清理Trace 8→ 移除State.Attacking 完整数据流就是：\n1输入 2→ ASC 3→ GameplayAbility 4→ AbilityTask播放动画 5→ GameplayEvent开启攻击检测 6→ TargetData 7→ GameplayEffectSpec 8→ Execution Calculation 9→ AttributeSet 10→ GameplayCue 11→ EndAbility 三、GAS的多人网络逻辑 GAS并不是让客户端直接决定伤害，而是在服务器权威的基础上，提供本地预测来改善操作手感。\nLocal Predicted 玩家按下翻滚时：\n客户端立即播放翻滚和预测性状态； 同时向服务器申请激活； 服务器重新检查体力、冷却、Tag和权限； 服务器接受则继续； 服务器拒绝则客户端撤销可回滚的预测结果。 官方提供四种主要Net Execution Policy：\nLocal Predicted； Local Only； Server Initiated； Server Only。(Epic Games Developers) 哪些技能适合Local Predicted 普通攻击； 翻滚；\n-冲刺； 跳跃； 本地玩家频繁使用且对延迟敏感的技能。 哪些适合Server Only或Server Initiated AI能力； 服务器控制的环境机关； 权威掉落； 只在服务端完成的被动结算； 不需要客户端即时响应的逻辑。 需要注意：可预测的非Instant GE通常更容易在服务器拒绝时回滚，而伤害等Instant属性变化不能简单依赖同样的回滚方式。因此通常让客户端预测动作、动画和部分状态，最终伤害仍由服务器权威确认。(Epic Games Developers)\n四、Ability的实例化策略 GAS提供三种Ability Instancing Policy。\nNon-Instanced 使用Ability的CDO执行，不生成实例。\n优点：\n开销最低。 限制：\n不能保存运行时成员状态； 不能绑定依赖实例的Delegate； 不能使用蓝图图表执行核心逻辑； 不能复制成员变量或处理Ability自身RPC。 适合大规模单位频繁执行、且完全无状态的简单能力。\nInstanced Per Actor 每个Actor持有一个Ability实例，多次执行重复使用。\n优点：\n可保存成员变量； 适合复制和RPC； 不会每次释放都创建新UObject。 缺点：\n每次激活前必须清理上一次残留状态。 大多数角色技能可以优先考虑这一策略。\nInstanced Per Execution 每次激活都创建一个新实例。\n优点：\n每次状态完全独立； 可以让同一Ability并发执行多份。 缺点：\n对象创建成本最高。 适合低频、复杂、可能并发执行的能力。(Epic Games Developers)\n五、GAS最常见的错误 1. 忘记EndAbility 结果：\nAbility永远处于Active； 激活标签不消失； 被它阻塞的其他Ability无法使用； AbilityTask和Delegate不能正常结束。 2. 在GameplayCue里写玩法逻辑 Cue可能丢失，只能用于表现，不能负责扣血或决定死亡。\n3. 直接修改Health 例如：\n1Health -= Damage; 这样会绕过：\nGE上下文； 网络预测； Effect Tag； 属性回调； 伤害来源记录；\n-统一结算流程。 复杂项目中应尽量通过GE修改GAS Attribute。\n4. 所有状态都做成Attribute Health适合Attribute，但“眩晕”“正在攻击”“无敌”更适合GameplayTag。\n一般：\n1连续数值 → Attribute 2离散状态 → GameplayTag 3行为流程 → GameplayAbility 4数值变化 → GameplayEffect 5外观反馈 → GameplayCue 5. 在Ability中堆积所有伤害公式 伤害公式应尽量放在可复用的MMC或Execution Calculation中，而不是每个攻击GA各写一次。\n6. Ability取消时没有清理外部资源 需要清理：\nMontage； 碰撞检测； 定时器； 自定义Delegate； 临时生成的Actor； 锁定目标； 输入模式。 AbilityTask能够随Ability结束，但Ability之外手工创建的资源仍需要自己管理。\n六、总结GAS： GAS以ASC为中枢，用GA组织行为流程，用GE修改Attribute和Tag，用AbilityTask处理跨帧时序，用GameplayCue分离表现，并通过服务器权威与本地预测支持多人游戏。\n原文：GAS\n","permalink":"https://jiat-t.github.io/ue/gameplay-ability-system/","summary":"整理 GAS 的核心组件、技能生命周期、属性与效果、异步任务及多人预测逻辑。","title":"UE Gameplay Ability System（GAS）"},{"content":"第六章的核心目标，是学习 Vulkan 1.1、Vulkan 1.2 和 Vulkan 1.3 中加入的新特性，以及如何在程序中查询、启用和使用这些特性。\n这一章主要包含以下内容：\n如何查询 Vulkan Loader 支持的最高版本 如何查询物理设备支持的特性 如何使用 pNext 扩展结构体 如何启用新版本 Vulkan 功能 如何使用无图像帧缓冲 如何使用动态渲染 如何在不依赖传统 Render Pass 和 Framebuffer 的情况下完成绘制 第六章与第七章的侧重点不同，\n第七章主要解决：数据如何进入 GPU\n第六章主要解决：如何使用现代 Vulkan 功能组织渲染流程\n这一章最重要的内容是：\n1Ch6-0 新版本特性查询 2Ch6-2 动态渲染 一、Ch6-0 使用新版本特性 Vulkan 在后续版本中不断增加新的功能。\n例如：\nVulkan 1.1 Vulkan 1.2 Vulkan 1.3 各种 KHR 扩展 各种 EXT 扩展 厂商扩展 这些新功能通常不能直接使用。\n在使用之前需要完成：\n1确认 Loader 支持 2→ 确认物理设备支持 3→ 查询对应 Feature 4→ 创建逻辑设备时启用 5→ 获取对应函数 6→ 正式使用 1. Vulkan Loader 版本 Vulkan 程序通常不会直接与显卡驱动中的 Vulkan 实现交互。\n程序首先通过 Vulkan Loader 加载 Vulkan API。\nLoader 支持的 Vulkan 版本可以通过：\n1vkEnumerateInstanceVersion(...) 查询。\n例如：\n1uint32_t apiVersion = VK_API_VERSION_1_0; 2 3if (vkEnumerateInstanceVersion != nullptr) { 4 vkEnumerateInstanceVersion(\u0026amp;apiVersion); 5} 版本号可以通过以下宏拆分：\n1VK_API_VERSION_MAJOR(apiVersion) 2VK_API_VERSION_MINOR(apiVersion) 3VK_API_VERSION_PATCH(apiVersion) 需要注意：\n1Loader 支持 Vulkan 1.3 2不代表当前 GPU 一定支持 Vulkan 1.3 Loader 版本表示当前运行环境能够识别和加载的 Vulkan API 版本。\n物理设备版本表示具体 GPU 和驱动实际支持的 Vulkan 版本。\n2. VkApplicationInfo 中的 API 版本 创建 Vulkan Instance 时，需要填写：\n1VkApplicationInfo 其中：\n1apiVersion 表示程序希望使用的 Vulkan API 版本。\n例如：\n1VkApplicationInfo applicationInfo{}; 2applicationInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; 3applicationInfo.apiVersion = VK_API_VERSION_1_3; 但是不能直接假设运行环境一定支持 Vulkan 1.3。\n更合理的做法是：\n1查询 Loader 最高版本 2→ 根据程序需求选择版本 3→ 不能超过 Loader 支持版本 例如：\n1uint32_t requestedVersion = VK_API_VERSION_1_3; 2uint32_t finalVersion = std::min(loaderVersion, requestedVersion); 3. 物理设备 API 版本 物理设备属性中包含：\n1VkPhysicalDeviceProperties::apiVersion 可以通过：\n1vkGetPhysicalDeviceProperties(...) 查询。\n也可以使用新版接口：\n1vkGetPhysicalDeviceProperties2(...) 设备支持版本与 Loader 支持版本必须同时考虑。\n可以简单理解为：\n1Loader Version 2= 当前系统最多能够加载哪个 Vulkan 版本 3 4Physical Device Version 5= 当前 GPU 和驱动实际实现了哪个 Vulkan 版本 6 7Application Version 8= 程序希望使用哪个 Vulkan 版本 最终可用版本受到三者共同限制。\n二、Feature、Property 与 Extension Vulkan 中经常会遇到三类设备能力信息：\n1Feature 2Property 3Extension 1. Feature Feature 表示某项功能是否受到支持。\n例如：\n1samplerAnisotropy 2geometryShader 3tessellationShader 4dynamicRendering 5imagelessFramebuffer Feature 通常是布尔值：\n1VK_TRUE 2VK_FALSE 例如：\n1samplerAnisotropy = VK_TRUE 表示设备支持各向异性过滤。\n但是：\n1支持某项 Feature 2不等于已经启用某项 Feature 创建逻辑设备时，只有显式开启的 Feature 才能使用。\n2. Property Property 表示设备的属性或限制。\n例如：\n1maxImageDimension2D 2maxPushConstantsSize 3minUniformBufferOffsetAlignment 4maxSamplerAnisotropy 这些通常不是开关，而是具体数值。\n例如：\n1maxPushConstantsSize = 256 表示设备允许的 Push Constant 最大容量为 256 字节。\nProperty 只能查询，不能开启。\n3. Extension Extension 表示 Vulkan 核心版本之外提供的额外功能。\n例如：\n1VK_KHR_swapchain 2VK_KHR_dynamic_rendering 3VK_KHR_create_renderpass2 扩展通常需要：\n查询是否支持 在创建 Instance 或 Device 时添加扩展名称 查询对应 Feature 开启对应 Feature 使用扩展函数 部分扩展会在后续 Vulkan 版本中提升为核心功能。\n例如：\n1VK_KHR_dynamic_rendering 后来成为 Vulkan 1.3 核心功能。\n三、VkPhysicalDeviceFeatures2 旧版 Vulkan 使用：\n1VkPhysicalDeviceFeatures 查询和开启设备功能。\n随着功能越来越多，单个结构体无法容纳所有新特性。\n因此 Vulkan 引入：\n1VkPhysicalDeviceFeatures2 其核心作用是：\n1通过 pNext 链连接其他 Feature 结构体 例如：\n1VkPhysicalDeviceFeatures2 features2{}; 2features2.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; 然后通过 pNext 连接 Vulkan 1.1、1.2 和 1.3 Feature：\n1VkPhysicalDeviceVulkan11Features features11{}; 2VkPhysicalDeviceVulkan12Features features12{}; 3VkPhysicalDeviceVulkan13Features features13{}; 连接关系可以写成：\n1VkPhysicalDeviceFeatures2 2→ VkPhysicalDeviceVulkan11Features 3→ VkPhysicalDeviceVulkan12Features 4→ VkPhysicalDeviceVulkan13Features 例如：\n1features2.pNext = \u0026amp;features11; 2features11.pNext = \u0026amp;features12; 3features12.pNext = \u0026amp;features13; 4features13.pNext = nullptr; 最后调用：\n1vkGetPhysicalDeviceFeatures2( 2 physicalDevice, 3 \u0026amp;features2 4); 设备会将支持情况写入每个结构体。\n四、pNext 链 pNext 是 Vulkan 中极其重要的扩展机制。\n大量 Vulkan 结构体都包含：\n1const void* pNext; 或者：\n1void* pNext; pNext 用于在不修改原有结构体定义的情况下，为 API 添加新功能。\n可以理解为：\n1基础结构体 2→ 扩展结构体 A 3→ 扩展结构体 B 4→ 扩展结构体 C 每个结构体通常包含：\n1sType 2pNext 其中：\nsType 表示当前结构体的真实类型 pNext 指向链中的下一个结构体 1. pNext 链示例 例如查询动态渲染支持：\n1VkPhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeatures{}; 2dynamicRenderingFeatures.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES; 然后连接到：\n1VkPhysicalDeviceFeatures2 features2{}; 2features2.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2; 4features2.pNext = \u0026amp;dynamicRenderingFeatures; 最后：\n1vkGetPhysicalDeviceFeatures2( 2 physicalDevice, 3 \u0026amp;features2 4); 查询完成后：\n1dynamicRenderingFeatures.dynamicRendering 会表示设备是否支持动态渲染。\n2. 为什么需要 pNext 如果没有 pNext，每增加一个 Vulkan 新功能，都可能需要修改原有结构体。\n这样会破坏：\nAPI 兼容性 二进制兼容性 旧代码 旧驱动 通过 pNext，Vulkan 可以保持旧结构体不变，同时不断加入新功能。\n因此：\n1pNext 是 Vulkan 的结构体扩展机制 以后学习以下功能时都会频繁使用：\nDynamic Rendering Synchronization2 Descriptor Indexing Buffer Device Address Ray Tracing Mesh Shader Timeline Semaphore 3. pNext 链注意事项 构建 pNext 链时需要注意：\n每个结构体必须填写正确的 sType 最后一个结构体的 pNext 必须为 nullptr 结构体生命周期必须覆盖 API 调用过程 不要将同一个结构体重复加入链 不要错误地形成循环链表 不要使用已经离开作用域的局部变量 查询结构和启用结构不能随意混用 同一类结构通常不应重复出现 例如，以下结构体不能在调用前被销毁：\n1VkPhysicalDeviceFeatures2 features2; 2VkPhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeatures; 因为 features2.pNext 指向后者。\n五、查询与启用的区别 Vulkan 中必须区分：\n1查询功能支持 2创建设备时启用功能 查询过程通常使用：\n1vkGetPhysicalDeviceFeatures2(...) 创建逻辑设备时则将需要开启的结构体连接到：\n1VkDeviceCreateInfo::pNext 例如：\n1VkPhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeatures{}; 2dynamicRenderingFeatures.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES; 4dynamicRenderingFeatures.dynamicRendering = VK_TRUE; 然后：\n1VkDeviceCreateInfo deviceCreateInfo{}; 2deviceCreateInfo.sType = 3 VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; 4deviceCreateInfo.pNext = 5 \u0026amp;dynamicRenderingFeatures; 需要遵守：\n1先查询 2→ 确认支持 3→ 再设置为 VK_TRUE 4→ 创建逻辑设备 不能在设备不支持时强行开启。\n1. 支持但未启用 例如设备返回：\n1dynamicRendering = VK_TRUE; 只表示设备支持动态渲染。\n如果创建逻辑设备时没有将：\n1dynamicRendering = VK_TRUE; 加入启用链，那么逻辑设备仍然不能合法使用该功能。\n因此：\n1Supported Feature 2≠ Enabled Feature 六、VkPhysicalDeviceProperties2 新版属性查询使用：\n1VkPhysicalDeviceProperties2 与 VkPhysicalDeviceFeatures2 类似，它也可以通过 pNext 连接更多属性结构体。\n例如：\n1VkPhysicalDeviceProperties2 properties2{}; 2properties2.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; 然后调用：\n1vkGetPhysicalDeviceProperties2( 2 physicalDevice, 3 \u0026amp;properties2 4); 基础属性位于：\n1properties2.properties 其中包括：\nGPU 名称 GPU 类型 API 版本 驱动版本 Vendor ID Device ID 设备限制 例如：\n1properties2.properties.deviceName 2properties2.properties.apiVersion 3properties2.properties.deviceType 4properties2.properties.limits 1. Features2 与 Properties2 的区别 1Features2 2用于查询功能是否受到支持 3 4Properties2 5用于查询设备属性和具体限制 例如：\n1samplerAnisotropy 2属于 Feature 3 4maxSamplerAnisotropy 5属于 Property 使用各向异性过滤时，需要同时检查：\n1Feature：是否支持各向异性过滤 2Property：最大允许使用多大的各向异性倍率 七、VkPhysicalDeviceMemoryProperties2 新版内存属性查询使用：\n1VkPhysicalDeviceMemoryProperties2 例如：\n1VkPhysicalDeviceMemoryProperties2 memoryProperties2{}; 2memoryProperties2.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2; 然后调用：\n1vkGetPhysicalDeviceMemoryProperties2( 2 physicalDevice, 3 \u0026amp;memoryProperties2 4); 实际内存属性位于：\n1memoryProperties2.memoryProperties 其中包含：\nMemory Type Memory Heap Memory Property Flags Heap Size 它与旧版：\n1vkGetPhysicalDeviceMemoryProperties(...) 的核心功能相同，但可以通过 pNext 查询更多扩展内存信息。\n八、Ch6-1 无图像帧缓冲 传统 Vulkan Framebuffer 在创建时，必须直接指定具体的 Image View。\n例如：\n1Framebuffer 0 2→ Swapchain Image View 0 3 4Framebuffer 1 5→ Swapchain Image View 1 6 7Framebuffer 2 8→ Swapchain Image View 2 交换链有几张图像，通常就需要创建几个 Framebuffer。\n1. 传统 Framebuffer 创建普通 Framebuffer 时，需要提供：\n1VkFramebufferCreateInfo 其中：\n1pAttachments 直接指向具体的 Image View。\n例如：\n1framebufferCreateInfo.attachmentCount = 1; 2framebufferCreateInfo.pAttachments = 3 \u0026amp;swapchainImageView; 因此 Framebuffer 与具体 Image View 绑定。\n2. Imageless Framebuffer 无图像帧缓冲的英文名称为：\n1Imageless Framebuffer 它并不是完全没有图像。\n它只是：\n1创建 Framebuffer 时 2不立即绑定具体 Image View 创建时只描述附件必须满足的条件：\n格式 Usage 宽度 高度 图层数 View Format 真正开始 Render Pass 时，再提供当前实际使用的 Image View。\n3. 启用 Imageless Framebuffer 首先需要查询并开启：\n1imagelessFramebuffer 对应结构体通常为：\n1VkPhysicalDeviceImagelessFramebufferFeatures 例如：\n1VkPhysicalDeviceImagelessFramebufferFeatures imagelessFeatures{}; 2imagelessFeatures.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGELESS_FRAMEBUFFER_FEATURES; 查询支持后，在创建逻辑设备时设置：\n1imagelessFeatures.imagelessFramebuffer = VK_TRUE; 4. 创建无图像 Framebuffer 创建 Framebuffer 时，需要设置：\n1VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT 例如：\n1VkFramebufferCreateInfo framebufferCreateInfo{}; 2framebufferCreateInfo.sType = 3 VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; 4framebufferCreateInfo.flags = 5 VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT; 然后通过：\n1VkFramebufferAttachmentsCreateInfo 描述所有附件。\n每个附件使用：\n1VkFramebufferAttachmentImageInfo 描述。\n其中包含：\nImage Usage Width Height Layer Count View Format 例如：\n1这个附件必须是颜色附件 2格式必须与交换链格式一致 3尺寸必须与交换链尺寸一致 4图层数为 1 5. 开始 Render Pass 时提供 Image View 开始 Render Pass 时，通过：\n1VkRenderPassAttachmentBeginInfo 提供实际使用的 Image View。\n然后将其连接到：\n1VkRenderPassBeginInfo::pNext 整体流程为：\n1创建 Framebuffer 2→ 只描述附件要求 3→ 不绑定具体 Image View 4 5开始 Render Pass 6→ 提供当前交换链 Image View 7→ 使用同一个 Framebuffer 因此多个交换链图像理论上可以共用同一个 Imageless Framebuffer。\n6. Imageless Framebuffer 优点 优点：\n减少 Framebuffer 对象数量 Framebuffer 不再固定绑定某个 Image View 可以延迟决定实际附件 某些场景下资源组织更灵活 7. Imageless Framebuffer 缺点 缺点：\n配置更加复杂 仍然需要 Render Pass 仍然需要 Framebuffer 对象 对现代渲染架构帮助有限 很多情况下节省的对象数量并不重要 后续 Dynamic Rendering 使用更直接 可以简单理解为：\n1Imageless Framebuffer 2只是让 Framebuffer 不再固定绑定 Image View 3 4Dynamic Rendering 5则直接取消了 Framebuffer 和 Render Pass 对象 因此 Ch6-1 更适合作为：\n1学习新特性启用和 pNext 链的练习 而不是后续渲染架构的重点。\n九、Ch6-2 动态渲染 动态渲染是第六章最重要的内容\n传统 Vulkan 绘制前需要创建：\n1VkRenderPass 2VkFramebuffer 录制命令时使用：\n1vkCmdBeginRenderPass(...) 2vkCmdEndRenderPass(...) 动态渲染改为：\n1vkCmdBeginRendering(...) 2vkCmdEndRendering(...) 不再需要传统的 Render Pass 和 Framebuffer 对象。\n十、传统 Render Pass 渲染流程 传统渲染流程通常为：\n1创建 Render Pass 2→ 创建 Framebuffer 3→ 创建 Graphics Pipeline 4→ Begin Render Pass 5→ Bind Pipeline 6→ Draw 7→ End Render Pass Render Pass 中提前定义：\n颜色附件 深度附件 模板附件 Load Operation Store Operation 初始布局 最终布局 Subpass Subpass Dependency Framebuffer 则负责绑定具体的 Image View。\n十一、动态渲染流程 动态渲染流程为：\n1创建 Graphics Pipeline 2→ 指定附件格式 3→ 准备 Rendering Attachment 4→ Begin Rendering 5→ Bind Pipeline 6→ Draw 7→ End Rendering 主要使用三个结构体：\n1VkPipelineRenderingCreateInfo 2VkRenderingAttachmentInfo 3VkRenderingInfo 十二、VkPipelineRenderingCreateInfo 使用动态渲染时，创建 Graphics Pipeline 不再提供传统 Render Pass。\n传统方式：\n1graphicsPipelineCreateInfo.renderPass = renderPass; 动态渲染中：\n1graphicsPipelineCreateInfo.renderPass = VK_NULL_HANDLE; 但是 Graphics Pipeline 仍然需要知道附件格式。\n因此使用：\n1VkPipelineRenderingCreateInfo 例如：\n1VkPipelineRenderingCreateInfo pipelineRenderingInfo{}; 2pipelineRenderingInfo.sType = 3 VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO; 4pipelineRenderingInfo.colorAttachmentCount = 1; 5pipelineRenderingInfo.pColorAttachmentFormats = 6 \u0026amp;swapchainFormat; 然后连接到：\n1VkGraphicsPipelineCreateInfo::pNext 整体关系为：\n1VkGraphicsPipelineCreateInfo 2→ pNext 3→ VkPipelineRenderingCreateInfo 如果存在深度附件，还需要填写：\n1depthAttachmentFormat 如果存在模板附件，还需要填写：\n1stencilAttachmentFormat 十三、VkRenderingAttachmentInfo 每一个实际渲染附件通过：\n1VkRenderingAttachmentInfo 描述。\n颜色附件通常包括：\nImage View Image Layout Load Operation Store Operation Clear Value Resolve 相关信息 例如：\n1VkRenderingAttachmentInfo colorAttachment{}; 2colorAttachment.sType = 3 VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; 4colorAttachment.imageView = 5 swapchainImageViews[imageIndex]; 6colorAttachment.imageLayout = 7 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; 8colorAttachment.loadOp = 9 VK_ATTACHMENT_LOAD_OP_CLEAR; 10colorAttachment.storeOp = 11 VK_ATTACHMENT_STORE_OP_STORE; 1. Load Operation Load Operation 决定渲染开始时如何处理附件原有内容。\n常见值：\n1VK_ATTACHMENT_LOAD_OP_LOAD 2VK_ATTACHMENT_LOAD_OP_CLEAR 3VK_ATTACHMENT_LOAD_OP_DONT_CARE 含义：\n1LOAD 2保留并读取原有内容 3 4CLEAR 5使用 Clear Value 清空 6 7DONT_CARE 8不关心之前的内容 2. Store Operation Store Operation 决定渲染结束后是否保存结果。\n常见值：\n1VK_ATTACHMENT_STORE_OP_STORE 2VK_ATTACHMENT_STORE_OP_DONT_CARE 如果最终图像需要显示或继续使用，通常需要：\n1VK_ATTACHMENT_STORE_OP_STORE 十四、VkRenderingInfo 整个动态渲染过程通过：\n1VkRenderingInfo 描述。\n主要包含：\nRender Area Layer Count View Mask Color Attachment Depth Attachment Stencil Attachment 例如：\n1VkRenderingInfo renderingInfo{}; 2renderingInfo.sType = 3 VK_STRUCTURE_TYPE_RENDERING_INFO; 4renderingInfo.renderArea.offset = {0, 0}; 5renderingInfo.renderArea.extent = swapchainExtent; 6renderingInfo.layerCount = 1; 7renderingInfo.colorAttachmentCount = 1; 8renderingInfo.pColorAttachments = 9 \u0026amp;colorAttachment; 开始渲染：\n1vkCmdBeginRendering( 2 commandBuffer, 3 \u0026amp;renderingInfo 4); 结束渲染：\n1vkCmdEndRendering(commandBuffer); 十五、启用动态渲染 动态渲染在 Vulkan 1.3 中成为核心功能。\n如果使用 Vulkan 1.3，可以通过：\n1VkPhysicalDeviceVulkan13Features 启用。\n例如：\n1VkPhysicalDeviceVulkan13Features features13{}; 2features13.sType = 3 VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES; 4features13.dynamicRendering = VK_TRUE; 较低版本 Vulkan 中，可以通过：\n1VK_KHR_dynamic_rendering 扩展使用。\n此时需要：\n检查扩展是否支持 启用扩展 查询动态渲染 Feature 开启动态渲染 获取对应函数入口 十六、核心函数与扩展函数 在 Vulkan 1.3 中，核心函数名称为：\n1vkCmdBeginRendering 2vkCmdEndRendering 扩展版本可能为：\n1vkCmdBeginRenderingKHR 2vkCmdEndRenderingKHR 如果程序同时支持核心和扩展路径，需要明确选择。\n不能在未确认函数可用时直接调用。\n十七、动态渲染中的图像布局 传统 Render Pass 可以通过：\n1initialLayout 2finalLayout 3Subpass Dependency 隐式完成部分图像布局转换和同步。\n动态渲染没有传统 Render Pass，因此这些工作需要程序自己完成。\n渲染开始前，交换链图像通常需要转换为：\n1VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 渲染结束后，需要转换为：\n1VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 整体流程为：\n1PRESENT_SRC_KHR 2→ COLOR_ATTACHMENT_OPTIMAL 3→ 动态渲染 4→ PRESENT_SRC_KHR 十八、动态渲染中的 Image Barrier 布局转换通常需要使用：\n1VkImageMemoryBarrier 或者 Synchronization2 中的：\n1VkImageMemoryBarrier2 传统 Barrier 调用：\n1vkCmdPipelineBarrier(...) Synchronization2 调用：\n1vkCmdPipelineBarrier2(...) Barrier 需要明确：\n旧布局 新布局 源管线阶段 目标管线阶段 源访问权限 目标访问权限 Image Subresource Range 1. 呈现到颜色附件 渲染开始前：\n1旧布局： 2VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 3 4新布局： 5VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 目标访问通常需要包含：\n1VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT 目标阶段通常需要包含：\n1VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 2. 颜色附件到呈现 渲染完成后：\n1旧布局： 2VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 3 4新布局： 5VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 需要确保颜色附件写入完成后，图像才能交给呈现系统。\n十九、Dynamic Viewport 和 Scissor 交换链尺寸发生变化后，如果 Viewport 和 Scissor 被固定写入 Pipeline，可能需要重新创建 Graphics Pipeline。\n更灵活的方式是使用动态状态：\n1VK_DYNAMIC_STATE_VIEWPORT 2VK_DYNAMIC_STATE_SCISSOR 录制命令时调用：\n1vkCmdSetViewport(...) 2vkCmdSetScissor(...) 这样窗口尺寸变化后，只需要更新命令中的 Viewport 和 Scissor。\n不过附件格式发生变化时，仍可能需要重新创建 Pipeline。\n因为：\n1VkPipelineRenderingCreateInfo 中记录了附件格式。\n二十、动态渲染与深度附件 如果动态渲染使用深度缓冲，需要额外创建：\nDepth Image Depth Memory Depth Image View 深度附件使用另一个：\n1VkRenderingAttachmentInfo 例如：\n1VkRenderingAttachmentInfo depthAttachment{}; 2depthAttachment.sType = 3 VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO; 4depthAttachment.imageView = depthImageView; 5depthAttachment.imageLayout = 6 VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; 7depthAttachment.loadOp = 8 VK_ATTACHMENT_LOAD_OP_CLEAR; 9depthAttachment.storeOp = 10 VK_ATTACHMENT_STORE_OP_DONT_CARE; 然后：\n1renderingInfo.pDepthAttachment = 2 \u0026amp;depthAttachment; Pipeline 创建时也必须填写正确的：\n1depthAttachmentFormat 二十一、Render Pass 与 Dynamic Rendering 的区别 1. 传统 Render Pass 需要：\n1VkRenderPass 2VkFramebuffer 3VkRenderPassBeginInfo 4vkCmdBeginRenderPass 5vkCmdEndRenderPass 优点：\n能够提前描述完整渲染流程 适合复杂 Subpass 某些移动 GPU 可利用 Subpass 优化 教程和参考代码丰富 缺点：\n对象较多 配置复杂 与附件格式和 Framebuffer 绑定较紧 交换链重建时涉及资源较多 2. Imageless Framebuffer 需要：\n1VkRenderPass 2VkFramebuffer 3VkRenderPassAttachmentBeginInfo 4vkCmdBeginRenderPass 5vkCmdEndRenderPass 区别是：\n1Framebuffer 创建时不固定 Image View 优点：\n一个 Framebuffer 可以适配多个 Image View 附件绑定更灵活 缺点：\n仍然需要 Render Pass 仍然需要 Framebuffer 配置并没有明显简化 实际收益有限 3. Dynamic Rendering 需要：\n1VkPipelineRenderingCreateInfo 2VkRenderingAttachmentInfo 3VkRenderingInfo 4vkCmdBeginRendering 5vkCmdEndRendering 不需要：\n1VkRenderPass 2VkFramebuffer 优点：\n对象更少 渲染流程更直接 附件可以在录制命令时指定 适合现代渲染架构 交换链重建更加简单 更适合模块化渲染 Pass 缺点：\n需要自己处理布局转换 需要更明确地处理同步 复杂 Subpass 场景仍需考虑传统 Render Pass 必须确认设备版本和功能支持 二十二、三种方式的整体对比 1传统 Render Pass： 2 3提前创建 Render Pass 4提前创建 Framebuffer 5Render Pass 隐式处理部分布局和依赖 6适合传统固定渲染流程 1Imageless Framebuffer： 2 3保留 Render Pass 4保留 Framebuffer 5Framebuffer 不固定具体 Image View 6实际开始渲染时再提供附件 1Dynamic Rendering： 2 3不创建 Render Pass 4不创建 Framebuffer 5开始渲染时直接指定附件 6布局转换和同步由程序显式处理 可以概括为：\n1Legacy Render Pass 2= 固定渲染描述 + 固定附件 3 4Imageless Framebuffer 5= 固定渲染描述 + 延迟指定附件 6 7Dynamic Rendering 8= 渲染时动态指定附件和操作 二十三、交换链重建差异 1. 传统 Render Pass 交换链重建时通常需要考虑：\nSwapchain Swapchain Image View Framebuffer Render Pass Graphics Pipeline Depth Buffer Command Buffer 如果交换链格式没有变化，Render Pass 和 Pipeline 有时可以保留。\n2. Imageless Framebuffer 仍然需要考虑：\nSwapchain Swapchain Image View Imageless Framebuffer 的尺寸和附件条件 Graphics Pipeline Command Buffer 即使 Framebuffer 不绑定具体 Image View，它仍然记录：\nWidth Height Layer Count Attachment Requirements 因此尺寸变化时通常仍需要重新创建。\n3. Dynamic Rendering 不需要重建：\nRender Pass Framebuffer 但仍需要处理：\nSwapchain Swapchain Image View Viewport Scissor Depth Image 屏幕尺寸相关附件 附件格式变化时的 Pipeline 因此动态渲染只是减少了一部分对象，并不代表交换链重建完全消失。\n二十四、第六章常见问题 1. 只查询 Feature，但没有启用 错误理解：\n1设备支持 Dynamic Rendering 2所以可以直接使用 正确流程：\n1查询支持 2→ 创建逻辑设备时启用 3→ 再使用 2. pNext 指向无效内存 例如：\n1void Setup() { 2 VkPhysicalDeviceDynamicRenderingFeatures features{}; 3 deviceCreateInfo.pNext = \u0026amp;features; 4} 函数结束后，features 被销毁。\n之后再使用：\n1deviceCreateInfo 就会产生悬空指针。\n3. sType 填写错误 每个扩展结构体必须填写对应的：\n1sType 错误的 sType 会导致驱动无法正确识别结构体。\n4. 动态渲染 Pipeline 没有指定附件格式 Dynamic Rendering 创建 Pipeline 时没有 Render Pass。\n因此必须通过：\n1VkPipelineRenderingCreateInfo 提供附件格式。\n否则 Pipeline 与实际附件不兼容。\n5. 没有进行交换链图像布局转换 动态渲染不会自动将交换链图像从呈现布局转换为颜色附件布局。\n必须显式完成：\n1PRESENT_SRC_KHR 2→ COLOR_ATTACHMENT_OPTIMAL 3→ PRESENT_SRC_KHR 6. Image Layout 与实际用法不一致 例如：\n1RenderingAttachmentInfo 中声明： 2COLOR_ATTACHMENT_OPTIMAL 3 4但图像实际仍然是： 5PRESENT_SRC_KHR 这会触发 Validation Layer 错误。\n7. 使用 vkDeviceWaitIdle 掩盖问题 在每帧调用：\n1vkDeviceWaitIdle(...) 虽然可能暂时避免资源竞争，但会严重降低并行能力。\n正确做法是使用：\nFence Semaphore Pipeline Barrier 正确的帧资源管理 二十五、第六章整体关系 第六章各节的关系为：\n1Ch6-0 2查询和启用新版本特性 3→ 学习 Feature、Property、Extension 和 pNext 4 5Ch6-1 6使用 Imageless Framebuffer 7→ 练习新特性启用和扩展结构体 8 9Ch6-2 10使用 Dynamic Rendering 11→ 移除传统 Render Pass 和 Framebuffer 12→ 显式处理附件、布局和同步 Ch6-0 是基础。\nCh6-1 和 Ch6-2 都依赖：\n版本查询 Feature 查询 pNext 链 逻辑设备 Feature 启用 二十六、第六章总结 第六章主要学习了 Vulkan 新版本功能的查询、启用和使用方法。\n首先通过 Ch6-0 学习：\n1Vulkan 版本查询 2Feature 查询 3Property 查询 4Memory Property 查询 5pNext 链 6逻辑设备 Feature 启用 然后通过 Ch6-1 学习：\n1Imageless Framebuffer 2延迟指定 Image View 3Framebuffer 扩展结构体 4Render Pass Begin 扩展结构体 最后通过 Ch6-2 学习：\n1Dynamic Rendering 2不再创建 Render Pass 3不再创建 Framebuffer 4渲染时动态指定附件 5显式处理布局和同步 第六章最核心的数据关系可以概括为：\n1查询设备能力 2→ 开启设备功能 3→ 创建兼容 Pipeline 4→ 准备渲染附件 5→ 转换图像布局 6→ Begin Rendering 7→ Draw 8→ End Rendering 9→ 转换为呈现布局 可以将第六章概括为：\n第六章解决了如何查询和开启现代 Vulkan 功能，并使用 Dynamic Rendering 替代传统 Render Pass 与 Framebuffer 完成绘制的问题。\n原文：EasyVulkan 第六章学习笔记\n","permalink":"https://jiat-t.github.io/vulkan/easyvulkan-chapter-6/","summary":"整理 Vulkan 新版本特性查询与启用、pNext 扩展链、无图像帧缓冲和动态渲染的使用流程。","title":"EasyVulkan 第六章学习笔记"},{"content":"第七章的核心目标，是在第二章已经能够绘制三角形的基础上，继续完善 Vulkan 中最基础的数据传递流程。\n这一章主要解决以下问题：\n如何将 CPU 中的顶点数据传入 GPU 如何使用索引复用顶点 如何一次绘制多个相同物体 如何向着色器传递少量数据 如何向着色器传递较大数据 如何将图片数据上传为 Vulkan 图像 如何在着色器中使用贴图 第七章完成后，整个程序将从“顶点写死在着色器中的简单三角形”，逐步扩展为一个具备顶点、索引、实例化、Uniform、Push Constant 和贴图能力的基础 Vulkan 渲染框架。\n一、Ch7-1 初识顶点缓冲区 在第二章中，三角形的顶点位置通常直接写在顶点着色器中，并通过 gl_VertexIndex 进行读取。\n这种方式只适合绘制极其简单的测试图形。\n真正的渲染程序中，顶点数据通常保存在 CPU 内存中，然后上传到 GPU 的顶点缓冲区中。\n整体流程为：\n1CPU 顶点数组 2→ 创建 VkBuffer 3→ 分配 VkDeviceMemory 4→ 上传顶点数据 5→ 绑定顶点缓冲区 6→ 顶点着色器读取数据 1. 顶点结构 顶点可以包含多个属性，例如：\n1struct Vertex 2{ 3 glm::vec2 position; 4 glm::vec3 color; 5 glm::vec2 texCoord; 6}; 这些数据通常交错存储在同一个顶点数组中。\n例如：\n1顶点0：位置、颜色、UV 2顶点1：位置、颜色、UV 3顶点2：位置、颜色、UV 2. VkBuffer 与 VkDeviceMemory Vulkan 中，Buffer 对象与实际内存是分离的。\n创建顶点缓冲区时，需要完成以下步骤：\n1创建 VkBuffer 2→ 查询内存需求 3→ 查找合适的内存类型 4→ 分配 VkDeviceMemory 5→ 将内存绑定到 Buffer VkBuffer 只描述缓冲区本身，例如：\n缓冲区大小 缓冲区用途 资源共享模式 真正存储数据的是 VkDeviceMemory。\n因此：\n1VkBuffer = 资源对象 2VkDeviceMemory = 实际内存 二者必须通过：\n1vkBindBufferMemory(...) 进行绑定。\n3. 顶点缓冲区用途 创建顶点缓冲区时，需要指定：\n1VK_BUFFER_USAGE_VERTEX_BUFFER_BIT 如果使用 Staging Buffer，则最终的顶点缓冲区通常还需要：\n1VK_BUFFER_USAGE_TRANSFER_DST_BIT 例如：\n1VK_BUFFER_USAGE_TRANSFER_DST_BIT | 2VK_BUFFER_USAGE_VERTEX_BUFFER_BIT 4. 顶点输入描述 Graphics Pipeline 必须知道顶点缓冲区中的数据如何排列。\n主要通过以下两个结构体进行描述：\n1VkVertexInputBindingDescription 2VkVertexInputAttributeDescription Binding Description Binding 描述整条顶点数据流。\n主要包含：\n1binding 2stride 3inputRate 例如：\n1binding = 0; 2stride = sizeof(Vertex); 3inputRate = VK_VERTEX_INPUT_RATE_VERTEX; 其中：\nbinding 表示当前数据使用哪个 Binding stride 表示相邻两个顶点之间的字节间隔 inputRate 表示数据按顶点更新还是按实例更新 Attribute Description Attribute 描述顶点结构中的某一个具体属性。\n主要包含：\n1location 2binding 3format 4offset 例如：\n1location = 0; 2binding = 0; 3format = VK_FORMAT_R32G32_SFLOAT; 4offset = offsetof(Vertex, position); 这里的 location 必须与顶点着色器中的输入位置一致：\n1layout(location = 0) in vec2 inPosition; 2layout(location = 1) in vec3 inColor; 3layout(location = 2) in vec2 inTexCoord; 因此：\n1Binding 描述一整条数据 2Attribute 描述数据中的某个成员 3Location 对应 Shader 中的输入位置 5. 绑定顶点缓冲区 绘制之前，需要调用：\n1vkCmdBindVertexBuffers(...) 之后再调用：\n1vkCmdDraw(...) 最终流程为：\n1创建顶点缓冲区 2→ 配置 Pipeline Vertex Input 3→ vkCmdBindVertexBuffers 4→ vkCmdDraw 6. Staging Buffer 性能较高的顶点缓冲区通常放在 Device Local 内存中。\n但是 Device Local 内存通常无法直接被 CPU 写入。\n因此需要使用 Staging Buffer。\n整体流程为：\n1CPU 数据 2→ Host Visible Staging Buffer 3→ vkCmdCopyBuffer 4→ Device Local Vertex Buffer Staging Buffer 通常使用：\n1VK_BUFFER_USAGE_TRANSFER_SRC_BIT 最终顶点缓冲区使用：\n1VK_BUFFER_USAGE_TRANSFER_DST_BIT | 2VK_BUFFER_USAGE_VERTEX_BUFFER_BIT Staging Buffer 只负责临时上传数据，上传完成后即可销毁。\n二、Ch7-2 初识索引缓冲区 索引缓冲区用于复用顶点。\n例如，一个矩形由两个三角形组成。如果不使用索引缓冲区，需要六个顶点：\n1三角形1：0、1、2 2三角形2：3、4、5 其中部分顶点的位置实际上是重复的。使用索引缓冲区后，只需要四个顶点：\n1Vertex vertices[] = 2{ 3 左上, 4 右上, 5 左下, 6 右下 7}; 再使用六个索引：\n1uint16_t indices[] = 2{ 3 0, 1, 2, 4 1, 3, 2 5}; 1. 索引缓冲区用途 索引缓冲区需要指定：\n1VK_BUFFER_USAGE_INDEX_BUFFER_BIT 如果使用 Staging Buffer，则通常为：\n1VK_BUFFER_USAGE_TRANSFER_DST_BIT | 2VK_BUFFER_USAGE_INDEX_BUFFER_BIT 2. 绑定索引缓冲区 绘制前调用：\n1vkCmdBindIndexBuffer(...) 其中必须指定索引类型：\n1VK_INDEX_TYPE_UINT16 或者：\n1VK_INDEX_TYPE_UINT32 索引类型必须与 CPU 中的索引数组类型保持一致。\n例如：\n1std::vector\u0026lt;uint16_t\u0026gt; 对应：\n1VK_INDEX_TYPE_UINT16 3. 索引绘制 绑定索引缓冲区后，使用：\n1vkCmdDrawIndexed(...) 代替：\n1vkCmdDraw(...) 整体流程为：\n1Vertex Buffer 提供顶点数据 2Index Buffer 提供顶点编号 3→ vkCmdBindVertexBuffers 4→ vkCmdBindIndexBuffer 5→ vkCmdDrawIndexed 索引缓冲区中保存的并不是顶点数据，而是顶点在顶点缓冲区中的编号。\n三、Ch7-3 实例化绘制 实例化绘制用于一次 Draw Call 绘制多个相同结构的物体。\n例如：\n大量草 大量树木 粒子 子弹 重复模型 大量相同网格 如果每个物体都单独调用一次 Draw，会产生大量 CPU 到 GPU 的绘制调用开销。\n实例化绘制可以通过一次调用绘制多个实例：\n1vkCmdDrawIndexed( 2 commandBuffer, 3 indexCount, 4 instanceCount, 5 0, 6 0, 7 0); 其中：\n1indexCount = 每个实例使用多少索引 2instanceCount = 绘制多少个实例 1. 顶点数据与实例数据 实例化绘制中，数据通常分为两类：\n1逐顶点数据 2逐实例数据 逐顶点数据包括：\n顶点位置 法线 UV 顶点颜色 逐实例数据包括：\n实例位置 实例颜色 实例缩放 模型矩阵 实例编号 2. Input Rate 顶点数据使用：\n1VK_VERTEX_INPUT_RATE_VERTEX 实例数据使用：\n1VK_VERTEX_INPUT_RATE_INSTANCE 例如：\n1VkVertexInputBindingDescription vertexBinding{}; 2vertexBinding.binding = 0; 3vertexBinding.stride = sizeof(Vertex); 4vertexBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; 实例数据：\n1VkVertexInputBindingDescription instanceBinding{}; 2instanceBinding.binding = 1; 3instanceBinding.stride = sizeof(InstanceData); 4instanceBinding.inputRate = VK_VERTEX_INPUT_RATE_INSTANCE; 其中：\n1VERTEX：每处理一个顶点读取下一条数据 2INSTANCE：每开始一个新实例读取下一条数据 3. gl_InstanceIndex Shader 中可以使用：\n1gl_InstanceIndex 获取当前实例编号。\n例如：\n1vec2 offset = offsets[gl_InstanceIndex]; 这样可以根据实例编号给每个实例设置不同位置。\n实例化绘制的核心意义是：\n1一次 Draw Call 2→ 绘制多个相同网格 3→ 每个实例拥有不同数据 四、Ch7-4 初识 Push Constant Push Constant 用于从 CPU 向 Shader 传递少量数据。\n它不需要：\n创建 Buffer 分配 Device Memory 创建 Descriptor Set 更新 Descriptor 只需要在 Pipeline Layout 中声明，然后在录制命令时写入。\n整体流程为：\n1定义 Push Constant 数据 2→ 创建 Push Constant Range 3→ 加入 Pipeline Layout 4→ Shader 声明 Push Constant 5→ vkCmdPushConstants 1. Push Constant Range 创建 Pipeline Layout 时，需要声明：\n1VkPushConstantRange pushConstantRange{}; 主要包含：\n1stageFlags 2offset 3size 例如：\n1pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; 2pushConstantRange.offset = 0; 3pushConstantRange.size = sizeof(PushConstantData); 2. Shader 中声明 Shader 中使用：\n1layout(push_constant) uniform PushConstantBlock 2{ 3 vec2 offset; 4 vec4 color; 5} pushConstant; C++ 中的结构体必须与 GLSL 中的数据布局对应。\n例如：\n1struct PushConstantData 2{ 3 glm::vec2 offset; 4 glm::vec4 color; 5}; 需要特别注意内存对齐。\n3. 写入 Push Constant 录制命令时调用：\n1vkCmdPushConstants(...) Push Constant 可以在 Draw Call 之间频繁修改。\n例如：\n1Push Constant A 2→ Draw Object A 3 4Push Constant B 5→ Draw Object B 因此它适合：\n每个物体的颜色 每个物体的位置 对象索引 少量模型参数 少量材质参数 4. Push Constant 特点 优点：\n使用简单 不需要 Descriptor 更新方便 适合每次绘制都变化的数据 缺点：\n容量较小 不适合传递大量数据 大量 Push Constant 更新会增加命令缓冲区内容 Vulkan 保证至少支持：\n1128 Bytes 实际限制可以通过：\n1maxPushConstantsSize 查询。\n五、Ch7-5 初识 Uniform Buffer Uniform Buffer 用于向 Shader 传递较大的只读数据。\n常见用途包括：\nModel 矩阵 View 矩阵 Projection 矩阵 相机位置 时间 灯光信息 全局参数 Uniform Buffer 本质上仍然是一个普通的 VkBuffer，但是用途被指定为：\n1VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT 1. Uniform Buffer 完整流程 Uniform Buffer 需要配合描述符使用。\n整体流程为：\n1创建 Uniform Buffer 2→ 创建 Descriptor Set Layout 3→ 创建 Descriptor Pool 4→ 分配 Descriptor Set 5→ 更新 Descriptor Set 6→ 绑定 Descriptor Set 7→ Shader 读取 Uniform Buffer 2. Descriptor Set Layout Descriptor Set Layout 用于描述 Shader 将会使用什么资源。\n例如：\n1VkDescriptorSetLayoutBinding uboLayoutBinding{}; 主要包含：\n1binding 2descriptorType 3descriptorCount 4stageFlags 例如：\n1uboLayoutBinding.binding = 0; 2uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 3uboLayoutBinding.descriptorCount = 1; 4uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; 这必须与 Shader 保持一致：\n1layout(set = 0, binding = 0) uniform UniformBufferObject 2{ 3 mat4 model; 4 mat4 view; 5 mat4 projection; 6} ubo; 3. Descriptor Pool Descriptor Pool 用于分配 Descriptor Set。\n创建时需要说明：\n可以分配多少个 Descriptor Set 每种 Descriptor 类型有多少个 例如：\n1VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER 4. Descriptor Set Descriptor Set 是实际绑定 Buffer 和 Shader 的对象。\n更新 Descriptor Set 时需要提供：\n1VkDescriptorBufferInfo 其中包含：\n1buffer 2offset 3range 然后通过：\n1vkUpdateDescriptorSets(...) 将 Uniform Buffer 写入 Descriptor Set。\n5. 绑定 Descriptor Set 绘制之前调用：\n1vkCmdBindDescriptorSets(...) 之后 Shader 才能读取对应 Uniform Buffer。\n整体关系为：\n1Shader Binding 2↕ 3Descriptor Set Layout 4↕ 5Descriptor Set 6↕ 7VkBuffer 6. 每帧 Uniform Buffer 如果程序存在多个 Frames In Flight，不建议所有帧共用同一个正在频繁修改的 Uniform Buffer。\n常见做法是：\n1Frame 0 → Uniform Buffer 0 2Frame 1 → Uniform Buffer 1 每帧拥有：\n一个 Uniform Buffer 一块 Memory 一个 Descriptor Set 这样可以避免 CPU 修改数据时，GPU 仍在读取同一块内存。\n7. Dynamic Uniform Buffer Dynamic Uniform Buffer 可以让多个对象共用同一个大 Buffer。\n数据排列为：\n1Object 0 Data 2Object 1 Data 3Object 2 Data 绑定 Descriptor Set 时，再通过动态偏移指定当前读取哪一段数据。\n动态偏移必须满足：\n1minUniformBufferOffsetAlignment 因此每段数据通常需要进行对齐。\n例如：\n1alignedSize = (dataSize + alignment - 1) \u0026amp; ~(alignment - 1); 8. Push Constant 与 Uniform Buffer 区别 1Push Constant： 2少量数据 3更新频繁 4不需要 Descriptor 5适合每次 Draw 不同的数据 6 7Uniform Buffer： 8数据较大 9需要 Descriptor 10适合矩阵、相机、灯光等全局数据 可以简单理解为：\n1Push Constant = 小而频繁 2Uniform Buffer = 大而稳定 六、Ch7-6 拷贝图像到屏幕 这一节开始正式接触 Vulkan Image。\nBuffer 与 Image 的区别：\n1Buffer： 2线性数据 3没有宽高 4适合顶点、索引、Uniform 5 6Image： 7二维或三维图像数据 8具有宽、高、格式、Mip Level 9适合贴图、渲染目标、深度缓冲 图像上传的整体流程为：\n1CPU 图片数据 2→ Staging Buffer 3→ VkImage 4→ 图像布局转换 5→ Copy 或 Blit 6→ 交换链图像 1. 创建 VkImage 创建 Image 时需要指定：\nImage Type Format Extent Mip Levels Array Layers Tiling Usage Initial Layout 例如：\n1VK_IMAGE_USAGE_TRANSFER_SRC_BIT 2VK_IMAGE_USAGE_TRANSFER_DST_BIT 3VK_IMAGE_USAGE_SAMPLED_BIT Image 与 Buffer 一样，也需要：\n1创建 VkImage 2→ 查询内存需求 3→ 分配 VkDeviceMemory 4→ vkBindImageMemory 2. Image Layout Vulkan Image 在不同用途下需要处于不同 Layout。\n常见 Layout 包括：\n1VK_IMAGE_LAYOUT_UNDEFINED 2VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL 3VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 4VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 5VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 6VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 例如：\n1准备接收复制 2→ TRANSFER_DST_OPTIMAL 3 4作为复制源 5→ TRANSFER_SRC_OPTIMAL 6 7被 Shader 读取 8→ SHADER_READ_ONLY_OPTIMAL 9 10用于屏幕显示 11→ PRESENT_SRC_KHR Image Layout 并不只是一个普通状态变量。\n它同时与以下内容有关：\n图像当前用途 GPU 内部存储方式 访问权限 同步关系 3. Image Memory Barrier 图像布局转换通常通过：\n1VkImageMemoryBarrier 配合：\n1vkCmdPipelineBarrier(...) 实现。\nBarrier 需要描述：\n旧布局 新布局 源访问权限 目标访问权限 源管线阶段 目标管线阶段 图像范围 例如：\n1UNDEFINED 2→ TRANSFER_DST_OPTIMAL 表示图像之后将作为复制目标。\n再例如：\n1TRANSFER_DST_OPTIMAL 2→ SHADER_READ_ONLY_OPTIMAL 表示图像复制完成后，将交给片段着色器读取。\n4. Copy Buffer To Image 通过：\n1vkCmdCopyBufferToImage(...) 可以将 Staging Buffer 中的像素数据复制到 Image。\n要求目标 Image 处于：\n1VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 5. Copy Image 与 Blit Image vkCmdCopyImage 用于直接复制图像。\nvkCmdBlitImage 除了复制外，还可以：\n缩放 翻转 过滤 在不同尺寸之间转换 例如，通过交换源图像上下坐标，可以实现垂直翻转。\n6. 拷贝到交换链 如果需要将图像复制到交换链图像，需要确保：\n1源图像 → TRANSFER_SRC_OPTIMAL 2交换链图像 → TRANSFER_DST_OPTIMAL 复制完成后，再将交换链图像转换为：\n1VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 才能进行显示。\n整体流程为：\n1Swapchain Image 2PRESENT_SRC_KHR 3→ TRANSFER_DST_OPTIMAL 4→ 接收图像 5→ PRESENT_SRC_KHR 七、Ch7-7 使用贴图 贴图本质上是一个可以被 Shader 读取的 Vulkan Image。\n完整流程为：\n1读取图片文件 2→ Staging Buffer 3→ Texture Image 4→ 生成 Mipmap 5→ 创建 Image View 6→ 创建 Sampler 7→ 创建 Combined Image Sampler Descriptor 8→ Shader 采样 1. Texture Image 贴图 Image 通常需要：\n1VK_IMAGE_USAGE_TRANSFER_DST_BIT 2VK_IMAGE_USAGE_SAMPLED_BIT 如果需要生成 Mipmap，还需要：\n1VK_IMAGE_USAGE_TRANSFER_SRC_BIT 因此常见用途为：\n1VK_IMAGE_USAGE_TRANSFER_SRC_BIT | 2VK_IMAGE_USAGE_TRANSFER_DST_BIT | 3VK_IMAGE_USAGE_SAMPLED_BIT 2. Image View Shader 不能直接使用 VkImage。\n必须先创建：\n1VkImageView Image View 用于描述：\n如何解释 Image 使用什么格式 使用哪些 Mip Level 使用哪些 Array Layer 使用哪个 Aspect 可以简单理解为：\n1VkImage = 实际图像资源 2VkImageView = 访问图像资源的视图 同一个 Image 可以创建多个不同的 Image View。\n3. Sampler Sampler 描述如何采样图像。\n包括：\n放大过滤 缩小过滤 Mipmap 过滤 UV 寻址模式 各向异性过滤 LOD 范围 常用参数包括：\n1magFilter 2minFilter 3mipmapMode 4addressModeU 5addressModeV 6addressModeW 7minLod 8maxLod 9maxAnisotropy 例如：\n1Image = 贴图数据本身 2Image View = 如何访问贴图 3Sampler = 如何对贴图进行采样 4. Combined Image Sampler 贴图通常通过：\n1VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER 传递给 Shader。\n需要创建：\n1VkDescriptorImageInfo 其中包含：\n1sampler 2imageView 3imageLayout 然后通过：\n1vkUpdateDescriptorSets(...) 写入 Descriptor Set。\n5. Shader 采样 片段着色器中声明：\n1layout(set = 0, binding = 1) uniform sampler2D textureSampler; 采样时使用：\n1vec4 color = texture(textureSampler, inTexCoord); 其中：\ntextureSampler 是贴图与采样器 inTexCoord 是 UV 返回值是采样得到的颜色 6. 纹理坐标 UV 通常在以下范围内：\n1U：0 到 1 2V：0 到 1 顶点结构中加入：\n1glm::vec2 texCoord; 顶点着色器将 UV 传给片段着色器：\n1layout(location = 2) in vec2 inTexCoord; 2layout(location = 0) out vec2 outTexCoord; 片段着色器接收：\n1layout(location = 0) in vec2 inTexCoord; 7. Mipmap Mipmap 是同一张贴图的多级缩小版本。\n例如：\n11024 × 1024 2512 × 512 3256 × 256 4128 × 128 5... 61 × 1 Mipmap 数量计算方式为：\n1floor(log2(max(width, height))) + 1 生成 Mipmap 时，可以通过：\n1vkCmdBlitImage(...) 逐级缩小。\n例如：\n1Mip 0 → Mip 1 2Mip 1 → Mip 2 3Mip 2 → Mip 3 生成过程中需要不断进行 Image Layout 转换。\n每一级在作为 Blit 源之前，需要转换为：\n1VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL 生成完成后，再转换为：\n1VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL Mipmap 的主要作用是：\n减少远处纹理闪烁 降低纹理缓存压力 提高缩小采样质量 提升性能 八、第七章整体关系 第七章的内容并不是相互独立的。\n整体数据流程为：\n1Ch7-1 2CPU 顶点数据 3→ Vertex Buffer 4→ Vertex Shader 5 6Ch7-2 7Index Buffer 8→ 复用顶点 9→ Indexed Draw 10 11Ch7-3 12Instance Data 13→ 一次绘制多个实例 14 15Ch7-4 16Push Constant 17→ 传递少量高频数据 18 19Ch7-5 20Uniform Buffer 21→ Descriptor Set 22→ 传递矩阵和全局数据 23 24Ch7-6 25Staging Buffer 26→ VkImage 27→ 图像复制和布局转换 28 29Ch7-7 30Texture Image 31→ Image View 32→ Sampler 33→ Descriptor Set 34→ Fragment Shader 九、各类数据的选择 1. 顶点缓冲区 适合：\n顶点位置 法线 UV 切线 顶点颜色 特点：\n1每个顶点读取一次 2. 索引缓冲区 适合：\n复用顶点 减少重复几何数据 特点：\n1保存顶点编号 3. 实例缓冲区 适合：\n大量相同网格 不同实例位置 不同实例颜色 不同实例模型矩阵 特点：\n1每个实例读取一次 4. Push Constant 适合：\n少量数据 高频修改 每个 Draw Call 都不同 例如：\n对象编号 小型变换参数 颜色 材质索引 5. Uniform Buffer 适合：\n矩阵 相机数据 灯光参数 全局参数 较大只读数据 6. Texture Image 适合：\n颜色贴图 法线贴图 深度贴图 渲染结果 各种二维图像数据 十、资源生命周期 Vulkan 中资源的销毁顺序非常重要。\nBuffer 的销毁顺序：\n1vkDestroyBuffer 2→ vkFreeMemory Image 的销毁顺序：\n1vkDestroyImageView 2→ vkDestroyImage 3→ vkFreeMemory Sampler：\n1vkDestroySampler Descriptor：\n1vkDestroyDescriptorPool 2vkDestroyDescriptorSetLayout Pipeline：\n1vkDestroyPipeline 2→ vkDestroyPipelineLayout 需要保证：\n1使用某资源的对象先销毁 2被依赖的资源后销毁 同时，在 GPU 仍可能使用资源时，不能直接销毁。\n通常需要等待：\n对应 Fence Queue Idle Device Idle 但不应该在每一帧都使用：\n1vkDeviceWaitIdle(...) 来掩盖同步问题。\n十一、总结 第七章主要完成了 Vulkan 中最基础的数据输入系统。\n从第二章中写死在 Shader 内部的三角形开始，逐步加入了：\n1顶点缓冲区 2索引缓冲区 3实例化绘制 4Push Constant 5Uniform Buffer 6Descriptor Set 7图像复制 8纹理采样 9Mipmap 最终形成了一条完整的数据传递链：\n1CPU 数据 2→ Vulkan Buffer 或 Image 3→ Device Memory 4→ Descriptor 或 Vertex Input 5→ Graphics Pipeline 6→ Shader 7→ 屏幕 可以将第七章概括为：\n第七章解决了几何数据、实例数据、常量数据和图像数据如何从 CPU 传递到 GPU，并最终被 Vulkan Graphics Pipeline 和 Shader 使用的问题。\n原文：EasyVulkan 第七章学习笔记\n","permalink":"https://jiat-t.github.io/vulkan/easyvulkan-chapter-7/","summary":"整理顶点与索引缓冲区、实例化绘制、Push Constant、Uniform Buffer 和贴图上传的数据传递流程。","title":"EasyVulkan 第七章学习笔记"},{"content":"来源包括：腾讯客户端岗 IEG 四面面经、腾讯 IEG 客户端四面面经、腾讯 IEG C++ 后台面经、游戏客户端方向面经总结、腾讯 PCG 客户端面经。\n一、操作系统高频\n1. 进程和线程的区别？\n进程是资源分配单位，线程是 CPU 调度单位。一个进程有独立的虚拟地址空间、堆、文件句柄等资源；一个进程可以包含多个线程，线程共享进程资源，但各自有自己的栈、寄存器和程序计数器。\n面试重点：\n进程切换开销更大，因为要切换地址空间、页表等资源。线程切换更轻量，因为同一进程内线程共享地址空间。\n游戏里主线程负责输入、逻辑、动画、AI 等，如果被网络请求、文件加载、大模型请求阻塞，就会卡帧。\n2. 线程共享哪些资源，独占哪些资源？\n共享：代码段、堆、全局变量、静态变量、文件句柄、进程地址空间。\n独占：线程栈、寄存器、程序计数器、线程局部存储 TLS。\n注意追问：\n因为堆和全局变量共享，所以多个线程同时读写同一数据时必须同步，否则会有数据竞争。\n3. 单核 CPU 上多线程为什么也能“同时运行”？\n单核 CPU 上多线程不是物理并行，而是并发。操作系统通过时间片轮转，让不同线程轮流获得 CPU。切换速度很快，所以看起来像同时运行。\n多核 CPU 才能真正并行，多个线程可以在不同核心上同时执行。\n面试官可能想听：\n并发是逻辑上的同时推进，并行是物理上的同时执行。线程太多不一定更快，因为调度、上下文切换、cache miss 和锁竞争都会增加。\n4. 上下文切换是什么？为什么有开销？\n上下文切换是 CPU 从一个线程/进程切换到另一个线程/进程时，保存当前执行现场并恢复另一个执行现场的过程。\n需要保存/恢复寄存器、程序计数器、栈指针等信息。进程切换还可能切换页表和地址空间，导致 TLB/cache 失效，所以开销更大。\n游戏开发里，线程不是越多越好。线程过多会让调度和同步成本超过并行收益。\n5. 线程同步方式有哪些？\n常见有 mutex、semaphore、condition_variable、atomic、读写锁、自旋锁。\nmutex：保护复杂临界区。atomic：适合简单变量的原子读写，比如计数器、状态位。semaphore：控制资源数量，比如对象池有 N 个可用资源。condition_variable：用于线程等待某个条件成立。\n面试回答：\n如果只是简单计数，用 atomic；如果保护一段复杂逻辑或容器，用 mutex；如果生产者消费者模型，可以用 mutex + condition_variable。\n6. 什么是死锁？条件是什么？怎么避免？\n死锁是多个线程互相等待对方持有的资源，导致所有线程都无法继续执行。\n四个必要条件：\n互斥占有且等待不可抢占循环等待\n避免方法：\n固定加锁顺序，破坏循环等待。一次性申请所需资源，破坏占有且等待。使用超时锁。减少锁粒度，避免嵌套锁。用 RAII 管理锁，避免忘记释放。\n7. 虚拟内存是什么？\n虚拟内存是操作系统给每个进程提供的独立地址空间。程序看到的是连续的虚拟地址，不直接访问物理内存。CPU 通过 MMU、TLB、页表把虚拟地址转换成物理地址。\n好处：\n进程隔离提高安全性按需加载支持换页让每个进程像拥有独立连续内存\n一句话：\n虚拟内存解决的是“程序地址空间”和“真实物理内存”之间的抽象与隔离问题。\n8. 虚拟地址到物理地址怎么转换？\nCPU 访问虚拟地址时，先查 TLB。TLB 是页表缓存，如果命中，直接得到物理页地址。\n如果 TLB 未命中，就查页表。页表记录虚拟页到物理页的映射。找到物理页后，加上页内偏移得到物理地址。\n如果页表发现这个页不在内存中，就触发缺页中断，由操作系统把数据从磁盘加载到内存。\n9. 缺页中断是什么？\n当进程访问的虚拟页没有映射到物理内存时，会触发缺页中断。\n操作系统会暂停当前进程，找到需要的数据页，把它从磁盘或交换区加载到物理内存，更新页表，然后恢复进程执行。\n如果访问的是非法地址，就不是正常缺页，而是访问违规或段错误。\n10. 堆和栈的区别？\n栈由系统自动管理，用于函数调用、局部变量、返回地址等。速度快，但空间有限，生命周期随函数返回结束。\n堆由程序动态申请，比如 new/malloc。空间更大，生命周期灵活，但需要手动或智能指针管理，容易泄漏和碎片。\n游戏里不要在栈上放很大的数组，也不要递归过深，否则容易栈溢出。\n11. malloc 分配的是虚拟内存还是物理内存？\nmalloc 返回的是虚拟地址。操作系统通常不会在 malloc 时立刻分配全部物理内存，而是按需分配。真正访问这块内存时，可能触发缺页中断，再分配物理页。\n所以可以说：\nmalloc 先在进程虚拟地址空间中分配区域，物理内存可能在实际访问时才分配。\n12. 怎么定位内存泄漏？\n先看现象：进程内存持续增长且不回落。\n常见方法：\n使用 ASan、Valgrind、Visual Studio Diagnostic Tools、Unreal Insights 等工具。检查 new/delete、malloc/free 是否匹配。检查 shared_ptr 循环引用。检查容器是否长期持有对象。检查委托/事件绑定后是否解绑。给对象构造/析构加计数日志。\nUE 项目里还要注意 UObject 引用链，某个 UPROPERTY 或委托引用可能导致对象一直无法被 GC。\n13. 野指针是什么？怎么避免？\n野指针是指向无效内存的指针。比如对象已经 delete，但指针还在被使用；或者返回了局部变量地址。\n避免方法：\n释放后置空。使用智能指针。明确对象所有权。避免返回局部变量地址。多线程场景中确保对象生命周期覆盖访问周期。UE 中 UObject 引用要用 UPROPERTY、TObjectPtr、TWeakObjectPtr 等方式管理。\n14. 进程间通信方式有哪些？\n常见 IPC：\n管道命名管道消息队列共享内存信号量Socket文件信号\n共享内存最快，因为多个进程直接映射同一块物理内存，不需要频繁数据拷贝。但它本身不提供同步，必须配合锁、信号量等机制。\n15. 用户态和内核态是什么？为什么要区分？\n用户态运行普通应用程序，权限受限，不能直接访问硬件和内核资源。内核态运行操作系统内核代码，权限高，可以管理内存、进程、设备和文件系统。\n区分的原因是安全和稳定。用户程序出错不能直接破坏内核。\n系统调用就是用户态进入内核态的一种方式，比如读写文件、网络 IO、创建线程等。\n二、计算机网络高频\n1. TCP 和 UDP 区别？\nTCP 是面向连接、可靠、有序、面向字节流的协议。它有三次握手、ACK、重传、流量控制、拥塞控制等机制。\nUDP 是无连接、不保证可靠、不保证顺序、面向报文的协议。它开销小、延迟低，但丢包、乱序、重复包都要应用层自己处理。\n游戏里：\n登录、支付、下载、聊天适合 TCP/HTTP。实时移动、战斗同步、帧输入更适合 UDP。关键事件可以在 UDP 上自己实现可靠层。\n2. TCP 三次握手过程？为什么必须三次？\n流程：\n客户端发送 SYN。服务端回复 SYN + ACK。客户端回复 ACK。\n目的：\n让双方确认自己和对方的发送、接收能力都正常。\n为什么不是两次：\n如果只有两次，服务端无法确认客户端是否收到了自己的 SYN + ACK，可能建立一个客户端并不知道的连接。三次握手可以避免历史连接请求导致的错误连接。\n3. TCP 四次挥手过程？为什么通常四次？\n流程：\n主动关闭方发送 FIN。被动关闭方回复 ACK。被动关闭方数据发送完后发送 FIN。主动关闭方回复 ACK。\n为什么通常四次：\nTCP 是全双工协议，两个方向可以独立关闭。一方不发送数据了，不代表另一方也没有数据要发送，所以 FIN 和 ACK 通常分开发。\n4. TCP 如何保证可靠传输？\n核心机制：\n序号：保证数据可排序、可去重。ACK：确认哪些数据已收到。超时重传：丢包后重新发送。滑动窗口：提高连续发送效率。校验和：检测数据损坏。拥塞控制：防止网络被打爆。流量控制：防止接收方缓冲区被打爆。\n一句话：\nTCP 的可靠性不是单一机制，而是序号、确认、重传、窗口和控制算法共同保证。\n5. 流量控制和拥塞控制区别？\n流量控制是接收方控制发送方，防止发送方发太快，把接收方缓冲区撑爆。主要靠接收窗口 rwnd。\n拥塞控制是发送方根据网络状态控制发送速度，防止网络过载。主要靠拥塞窗口 cwnd。\n区别：\n流量控制看接收方能力。拥塞控制看网络承载能力。\n6. TCP 拥塞控制有哪些阶段？\n经典阶段：\n慢启动拥塞避免快重传快恢复\n慢启动：cwnd 从小开始指数增长。拥塞避免：达到阈值后线性增长。快重传：收到多个重复 ACK，认为包丢了，不等超时，直接重传。快恢复：重传后不完全回到慢启动，而是降低窗口继续传输。\n面试说清楚即可，不用背每个公式。\n7. TIME_WAIT 是什么？为什么要等 2MSL？\n主动关闭 TCP 的一方会进入 TIME_WAIT 状态，等待 2MSL。\n作用：\n确保最后一个 ACK 能被对方收到。如果 ACK 丢了，对方会重发 FIN，主动关闭方还能再次回复 ACK。让旧连接中的残留报文在网络中消失，避免影响后续相同四元组的新连接。\nMSL 是报文最大生存时间，2MSL 覆盖一个来回的最大生命周期。\n8. TCP 粘包是什么？怎么解决？\nTCP 是字节流协议，没有消息边界。发送方多次 send 的数据，接收方可能一次 recv 收到；也可能一次 send 的数据被多次 recv 收到，这就是粘包/拆包问题。\n解决方法：\n固定长度消息。消息头里带 body 长度。特殊分隔符。应用层协议自己做拆包和缓存。\nUDP 是面向报文的，一次 sendto 对应一个 datagram，天然有报文边界，但可能丢包。\n9. UDP 如何实现可靠传输？\n应用层自己做可靠层：\n序号/帧号ACK 或 NACK超时重传去重乱序缓存滑动窗口心跳检测\n游戏里可以区分消息类型：\n移动包：高频发送，丢旧用新。关键事件：技能释放、购买、任务提交，必须可靠确认。状态快照：可以定期覆盖旧状态。\n10. 游戏为什么常用 UDP？\n实时游戏更重视低延迟。位置、朝向、输入等数据过期后价值很低，旧包即使重传回来也没意义。\nTCP 的可靠有序会导致队头阻塞：前面一个包丢了，后面的包即使到了也不能交付应用层，可能造成卡顿。\n所以游戏常用 UDP 做实时同步，再对关键消息做应用层可靠机制。\n11. 状态同步和帧同步区别？\n状态同步：\n服务器维护权威状态，把位置、血量、技能结果等同步给客户端。客户端做插值、预测、修正。优点是反作弊和重连好，适合动作、FPS、MMO。\n帧同步：\n服务器同步所有玩家输入，每个客户端在同一帧执行相同逻辑。优点是带宽低，适合 RTS、MOBA；缺点是要求强确定性，断线重连和反作弊更复杂。\n面试回答：\n动作游戏通常更偏状态同步或混合同步；策略类、MOBA 可以用帧同步。\n12. 断线重连怎么设计？\n客户端重连时带账号、房间号、角色 ID、最后确认帧号或状态版本号。\n服务器校验身份后：\n状态同步：下发当前权威快照，客户端恢复位置、血量、Buff、任务等状态。帧同步：补发缺失帧输入，或者发送当前快照后继续同步。\n还要处理：\n重复登录重连超时战斗中托管资源重新加载防作弊校验\n13. TCP 和 UDP 能不能绑定同一个端口？\n可以。TCP 和 UDP 是不同协议，端口空间相互独立。系统区分连接时会看协议类型、IP、端口等信息。\n所以 TCP 8080 和 UDP 8080 可以同时存在。\n14. select、poll、epoll 区别？\n它们都是 IO 多路复用，用一个线程监听多个 socket。\nselect：使用 fd_set，有数量限制，每次都要拷贝和线性扫描。poll：用数组保存 fd，没有 select 那种固定数量限制，但仍然要线性扫描。epoll：事件驱动，注册一次 fd，有事件时内核通知，适合大量连接。\n总结：\nselect 老、限制多；poll 去掉部分限制但仍线性扫描；epoll 更适合高并发。\n15. 非阻塞 IO 怎么设计？\n把 socket 设置为非阻塞，读写时不等待数据到达，而是立刻返回。\n通常配合 epoll/select：\n监听可读可写事件。有数据就读，没数据不阻塞。发送数据先放发送缓冲区，socket 可写时再继续发。应用层处理半包、粘包、超时、断开。\n游戏客户端里，网络 IO 不应该阻塞主线程，通常放在网络线程或异步任务中。\n16. 网络字节序是什么？为什么需要？\n网络字节序统一是大端序。不同 CPU 的主机字节序可能不同，比如 x86 常见是小端。\n如果传输多字节整数时不统一字节序，不同机器解析出来的值可能不同。\n所以发送前用 htonl/htons 转网络序，接收后用 ntohl/ntohs 转回主机序。\n你复试前最该背的优先级\nP0：\n进程 vs 线程线程共享/独占资源虚拟内存和页表堆 vs 栈死锁TCP vs UDP三次握手/四次挥手可靠传输拥塞控制 vs 流量控制UDP 可靠层游戏为什么用 UDP状态同步 vs 帧同步\nP1：\n上下文切换malloc 与虚拟内存内存泄漏/野指针select/poll/epoll粘包拆包TIME_WAIT断线重连\n如果面试官问得偏岗位，你要主动把网络题往游戏场景上靠：低延迟、可靠关键事件、服务器权威、断线恢复、防作弊。\n原文：计网 / OS\n","permalink":"https://jiat-t.github.io/cpp/network-and-os-notes/","summary":"整理进程与线程、同步与内存管理，以及网络协议和游戏客户端通信中的常见基础问题。","title":"计算机网络与操作系统笔记"},{"content":"下面按“面试可能问到的底层基础”来梳理，不展开到源码级，但要能说清楚 UE5 为什么这样设计、各模块怎么协作。\n1. UObject / 反射系统\n核心知识：\nUE 的很多对象都继承自 UObject，例如 Actor、ActorComponent、DataAsset、Widget 等。UObject 最大的价值不是普通 C++ 继承，而是接入了 UE 的反射、GC、序列化、蓝图、编辑器属性面板等系统。\n常见问法：\nUE 的反射系统是怎么来的？\nUCLASS、UPROPERTY、UFUNCTION 有什么用？\n为什么 UE 不直接用 C++ RTTI？\n标准回答：\nUE 通过 UHT，也就是 Unreal Header Tool，在编译前扫描带有 UCLASS、UPROPERTY、UFUNCTION 等宏的代码，生成额外的反射代码。反射信息会记录类名、父类、属性、函数、元数据等，使对象可以被编辑器识别、被蓝图调用、被序列化、被 GC 追踪。\nUE 不完全依赖 C++ RTTI，是因为引擎需要更强的跨平台、编辑器、蓝图、序列化和对象管理能力。C++ RTTI 只能解决运行时类型识别，不能满足属性面板、蓝图暴露、网络复制、存档等需求。\n2. UObject 生命周期与 GC\n核心知识：\nUE 的 UObject 由引擎对象系统管理，不能像普通 C++ 对象一样随便 new/delete。GC 会从 Root Set 出发，沿着 UPROPERTY 引用链标记可达对象，不可达对象会被回收。\n常见问法：\nUE 的 GC 怎么知道一个对象要不要回收？\n为什么 UObject 指针要写 UPROPERTY？\nMarkAsGarbage 是不是直接销毁对象？\n标准回答：\nUE GC 采用标记清除思路。它从 Root Set、World、Level、Actor、被 UPROPERTY 标记的引用等出发，遍历对象引用关系。能被访问到的对象保留，访问不到的对象会在 GC 阶段被回收。\nUPROPERTY 的意义是把对象引用暴露给反射系统和 GC。如果一个 UObject 指针只是普通 C++ 裸指针，GC 可能不知道这个引用存在，导致对象被误回收。MarkAsGarbage 只是标记对象可回收，不是立刻 delete。\n3. Actor / Component 架构\n核心知识：\nActor 是可以放进 World 的游戏对象，Component 是挂在 Actor 上的功能模块。UE 倾向于用组件组合功能，而不是所有功能都写进继承层级。\n常见问法：\nUObject 和 Actor 是什么关系？\nActor 和 ActorComponent 的关系是什么？\n为什么很多功能适合做成 Component？\n标准回答：\nActor 继承自 UObject，是一种可以存在于关卡中的对象，拥有 Transform、生命周期、Tick、网络复制等能力。ActorComponent 也是 UObject，但它不能独立放在场景中，通常挂在 Actor 上提供功能。\n使用 Component 的好处是组合优于继承。例如攻击系统、装备系统、交互系统、血量系统都可以做成 Component，不同角色按需挂载，复用性更好，也避免角色类越来越臃肿。\n4. World / Level / GameInstance\n核心知识：\nUE 的运行世界由 UWorld 管理，Level 是场景内容，GameInstance 是整个游戏进程生命周期内较持久的对象。\n常见问法：\nGameInstance、GameMode、GameState、PlayerController、Pawn 分别干什么？\n哪些只存在服务器？哪些客户端也有？\n标准回答：\nGameInstance 生命周期最长，切换关卡时也可以保留，适合保存全局状态。\nGameMode 只在服务器存在，负责游戏规则，比如玩家出生、胜负条件。\nGameState 服务器和客户端都有，用来同步公共游戏状态。\nPlayerController 代表玩家控制逻辑，处理输入和控制 Pawn。\nPawn 是可以被控制的角色或单位，Character 是 Pawn 的一种，内置移动组件和胶囊体。\n5. Tick 与引擎主循环\n核心知识：\nUE 每帧会执行输入、游戏逻辑、动画、物理、渲染提交等流程。Tick 是最常见的每帧更新入口，但不能滥用。\n常见问法：\nTick 是什么时候执行的？\n为什么不建议大量对象 Tick？\n如果不用 Tick，怎么实现定时逻辑？\n标准回答：\nTick 是对象每帧更新逻辑的入口，Actor、Component 都可以 Tick。但大量 Tick 会增加 Game Thread 压力，尤其是很多对象每帧做查询、寻路、Trace 或复杂判断时容易卡顿。\n可以用 Timer、事件驱动、AnimNotify、Behavior Tree Service、状态变化回调等方式替代 Tick。比如 AI 距离检测不需要每帧做，可以 0.3 到 0.5 秒更新一次。\n6. 蓝图与 C++ 通信\n核心知识：\nUE 的常见开发方式是 C++ 写底层和框架，蓝图做配置、表现和快速迭代。\n常见问法：\nC++ 和蓝图怎么通信？\nBlueprintCallable、BlueprintImplementableEvent、BlueprintNativeEvent 有什么区别？\n什么时候用 C++，什么时候用蓝图？\n标准回答：\nC++ 可以通过 UFUNCTION 暴露函数给蓝图，通过 UPROPERTY 暴露变量给编辑器和蓝图。BlueprintCallable 表示蓝图可以调用这个 C++ 函数；BlueprintImplementableEvent 表示 C++ 声明事件，蓝图实现；BlueprintNativeEvent 表示 C++ 可以提供默认实现，蓝图也可以重写。\n底层逻辑、性能敏感、多人同步、复杂系统框架适合 C++；UI、动画衔接、关卡脚本、数值配置、简单交互适合蓝图。\n7. 委托 / 事件系统\n核心知识：\n委托是 UE 里常用的解耦机制，相当于事件回调。一个系统发生变化后，不需要直接依赖另一个系统，只需要广播事件。\n常见问法：\nUE 委托有什么用？\n单播、多播、动态委托区别？\n标准回答：\n委托用于解耦模块。例如血量组件血量归零后广播 OnDeath，AI、UI、任务系统都可以监听，不需要血量组件直接调用它们。\n单播委托绑定一个回调，多播委托可以绑定多个回调。动态委托支持反射和蓝图绑定，但开销更高；普通 C++ 委托性能更好。\n8. 序列化与资源系统\n核心知识：\nUE 的序列化用于保存和加载对象数据，资源文件 .uasset 本质上也依赖 UE 的对象和序列化系统。\n常见问法：\nUE 序列化是干什么的？\n修改类成员后，已经保存的资源怎么兼容？\nDataAsset 和 DataTable 有什么区别？\n标准回答：\nUE 序列化负责把 UObject 的属性保存到磁盘，或者从磁盘加载回来。UPROPERTY 标记的属性可以被引擎识别并参与序列化、编辑器显示、复制等流程。\n如果修改了类成员，需要考虑旧资源兼容，比如给新字段默认值、保留旧字段迁移数据、使用版本号处理加载逻辑。\nDataAsset 更适合一个对象一份配置，比如武器、敌人、技能；DataTable 更适合表格化批量数据，比如大量物品或数值配置。\n9. 动画系统与 AnimNotify\n核心知识：\n游戏战斗里很多逻辑不是按键瞬间触发，而是跟动画时间点绑定。AnimNotify 用来在动画指定帧触发逻辑。\n常见问法：\n为什么攻击判定要放在 AnimNotify 里？\nMontage 有什么用？\n状态机和 Montage 区别？\n标准回答：\n攻击输入只代表玩家想攻击，但真正的伤害判定应该发生在武器挥到敌人那一段动画时间里，所以用 AnimNotify 打开 Trace 或碰撞窗口更合理。\nMontage 适合播放攻击、翻滚、受击、处决这类临时动作，可以插入 Notify、分段播放、打断和混合。动画状态机更适合待机、走路、跑步、跳跃等持续状态切换。\n10. 碰撞与 Trace\n核心知识：\nUE 碰撞主要涉及 Collision Channel、Object Type、Response、Overlap、Hit、Trace。\n常见问法：\nOverlap 和 Hit 区别？\nLineTrace、SphereTrace、碰撞体检测怎么选？\n高速子弹穿透怎么办？\n标准回答：\nOverlap 表示两个碰撞体发生重叠，不一定有物理阻挡；Hit 通常表示发生阻挡碰撞，有命中点、法线等信息。\nLineTrace 适合射线检测，比如枪械、视线检测；SphereTrace 有体积，更适合近战武器扫击或容错检测；Overlap 适合范围触发器。\n高速子弹如果只靠离散碰撞，可能一帧前在敌人前，一帧后在敌人后，直接穿过去。解决方法是使用连续检测，比如上一帧位置到当前帧位置做 LineTrace / SphereTrace，或者启用 CCD。\n11. AIController / Blackboard / Behavior Tree\n核心知识：\nUE AI 常见组合是 AIController 控制 Pawn，Blackboard 存状态，Behavior Tree 做决策。\n常见问法：\nBehavior Tree、Blackboard、Task、Service、Decorator 分别是什么？\n为什么不用 Tick 写 AI？\n标准回答：\nAIController 是 AI 的控制者，Blackboard 是行为树共享的数据表，比如 TargetActor、Distance、PatrolLocation。Behavior Tree 根据黑板状态选择行为。\nTask 执行具体动作，比如移动、攻击；Service 定时更新数据，比如目标距离；Decorator 判断条件，比如是否有目标、是否在攻击范围内。\n行为树比 Tick 里写大量 if else 更清晰，状态切换更可视化，也更适合策划和程序协作。\n12. NavMesh 与寻路\n核心知识：\nNavMesh 是把场景中可走区域生成导航网格，AI 寻路会在导航网格上计算路径。\n常见问法：\nNavMesh 是什么？\nAI 为什么 MoveTo 失败？\nA* 和 NavMesh 有什么关系？\n标准回答：\nNavMesh 表示角色可行走区域。AI MoveTo 时会根据目标点在 NavMesh 上搜索路径，然后沿路径移动。\nMoveTo 失败常见原因包括目标点不在 NavMesh 上、NavMesh Bounds Volume 没覆盖、角色碰撞体太大、Agent 半径/高度配置不匹配、目标不可达。\nA* 是常见寻路算法，NavMesh 提供图结构，A* 在图上搜索从起点到终点的较优路径。\n13. 网络复制基础\n核心知识：\nUE 网络模型通常是服务器权威。Actor 和属性可以复制，RPC 用于远程调用。\n常见问法：\nUE Replication 是什么？\nServer RPC、Client RPC、Multicast RPC 区别？\n为什么不能相信客户端？\n标准回答：\nReplication 是服务器把 Actor 或属性状态同步到客户端。服务器通常拥有权威状态，客户端负责表现和输入。\nServer RPC 是客户端请求服务器执行逻辑，例如释放技能；Client RPC 是服务器通知某个客户端；Multicast RPC 是服务器通知所有相关客户端。\n不能相信客户端是因为客户端容易被篡改。伤害、掉落、购买、技能命中等关键逻辑应该由服务器校验。\n14. 引擎线程模型\n核心知识：\nUE 不是所有工作都在一个线程里。常见有 Game Thread、Render Thread、RHI Thread，以及异步加载线程等。\n常见问法：\nGame Thread 和 Render Thread 分别干什么？\n为什么不能在主线程等网络请求？\n异步任务完成后怎么回到游戏线程？\n标准回答：\nGame Thread 负责游戏逻辑、Actor Tick、输入、AI 等。Render Thread 负责把渲染命令组织并提交给底层图形接口。RHI Thread 更接近底层图形 API 调用。\n网络请求、文件加载、AI 请求、耗时计算不能阻塞 Game Thread，否则会卡帧。异步完成后，如果要操作 UObject 或 UI，通常需要切回 Game Thread。\n15. 模块与插件系统\n核心知识：\nUE 工程和插件都可以拆成模块。插件可以有 Runtime Module 和 Editor Module。\n常见问法：\nUE 插件怎么设计？\nRuntime Module 和 Editor Module 区别？\n为什么工具代码不应该放进运行时模块？\n标准回答：\nRuntime Module 是游戏运行时需要的代码，可以打包进游戏；Editor Module 只在编辑器中使用，比如自定义面板、导入工具、资产检查工具。\n插件设计时，运行时能力应该放 Runtime，编辑器工具放 Editor，避免把编辑器依赖带进最终包体。对外接口可以用 Subsystem、ActorComponent 和 BlueprintCallable API 暴露。\n16. 渲染管线与 RHI\n核心知识：\nUE 渲染大体经过场景收集、可见性剔除、阴影、BasePass、光照、后处理、UI 等阶段。RHI 用来屏蔽不同图形 API 差异。\n常见问法：\nRHI 是干什么的？\nUE 为什么要有 Render Thread？\nLumen / Nanite 大概是什么？\n标准回答：\nRHI 是 Render Hardware Interface，负责抽象 DirectX、Vulkan、Metal 等底层图形 API，让上层渲染代码可以跨平台。\nRender Thread 把渲染提交和游戏逻辑分开，减少 Game Thread 被 GPU 提交细节拖慢。\nNanite 是 UE5 的虚拟化几何系统，用于高面数模型的高效渲染；Lumen 是动态全局光照和反射系统，用于更实时的间接光效果。\n17. 性能分析与调试\n核心知识：\n游戏客户端面试很容易问“卡了怎么查”。UE 里要知道 stat 命令、Unreal Insights、Profiler、Behavior Tree Debugger、碰撞调试。\n常见问法：\n游戏卡顿你怎么定位？\n怎么判断是 CPU 还是 GPU 瓶颈？\nAI 不动怎么查？\n标准回答：\n我会先复现问题，然后用 stat unit 看 Game、Draw、GPU 时间。如果 Game 高，优先看 Tick、AI、蓝图、碰撞、网络；如果 Draw 高，看渲染提交和物体数量；如果 GPU 高，看材质、后处理、阴影、透明物体、粒子。\nAI 问题可以看 Behavior Tree Debugger、Blackboard 值、NavMesh 可视化、MoveTo 返回结果和碰撞设置。\n你面试前最该背的优先级\n第一优先级：UObject / 反射 / GC / UPROPERTY。\n第二优先级：Actor / Component / GameMode / Controller / Pawn。\n第三优先级：蓝图 C++ 通信、委托、AnimNotify、碰撞 Trace。\n第四优先级：Behavior Tree / Blackboard / NavMesh。\n第五优先级：Tick 优化、对象池、异步、线程、Profiler。\n如果时间紧，你不需要把源码细节背到很深，但每个点都要能回答“它解决什么问题、核心机制是什么、在我的 Demo 里哪里用到了”。\n原文：可能的UE底层知识\n","permalink":"https://jiat-t.github.io/ue/ue-low-level-notes/","summary":"梳理 UObject、反射与 GC、Actor 和组件，以及 UE 各模块协作中的基础概念。","title":"可能的 UE 底层知识"},{"content":"通过这个小练习，完成后应该了解到的是：C++ 负责定义逻辑与接口，而 UE 编辑器则负责创建与摆放对象、调整参数以及最终 Shader 的实现。同时，也能够学习 UE C++ 怎么把变量暴露到编辑器、怎么创建组件、怎么引用资产、怎么生成动态材质实例、怎么给蓝图提供接口\n这个练习也非常简单：通过 C++ 创建一个 Actor 对象，让它出现在 UE 的编辑器里，并且我们能够通过编辑器的 Details 面板来调整这个 Actor 的颜色、粗糙度、自发光强度等等\n注：这里我使用的是 UE5.6 版本\n创建一个 C++ 类 点击编辑器上方的 工具 -\u0026gt; 新建 C++ 类 -\u0026gt; Actor，命名为“MaterialShowcaseActor”，之后创建\n此时 UE 会自动生成两个文件：MaterialShowcaseActor.h 和 MaterialShowcaseActor.cpp，与大多数类一样，前者用于声明，后者用于定义，模板如下：\n1#pragma once 2#include \u0026#34;CoreMinimal.h\u0026#34; 3#include \u0026#34;GameFramework/Actor.h\u0026#34; 4#include \u0026#34;MaterialShowcaseActor.generated.h\u0026#34; 5 6UCLASS() 7// 注意这里的类名是项目的名字 + \u0026#34;_API\u0026#34; 8class UE5_MATERIAL_LAB_API AMaterialShowcaseActor : public AActor 9{ 10 GENERATED_BODY() 11 12public:\t13 // Sets default values for this actor\u0026#39;s properties 14 AMaterialShowcaseActor(); 15 16protected: 17 // Called when the game starts or when spawned 18 virtual void BeginPlay() override; 19 20public:\t21 // Called every frame 22 virtual void Tick(float DeltaTime) override; 23}; 1#include \u0026#34;MaterialShowcaseActor.h\u0026#34; 2#include \u0026#34;Components/StaticMeshComponent.h\u0026#34; 3#include \u0026#34;Materials/MaterialInstanceDynamic.h\u0026#34; 4 5// Sets default values 6AMaterialShowcaseActor::AMaterialShowcaseActor() 7{ 8 // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don\u0026#39;t need it. 9 PrimaryActorTick.bCanEverTick = true; 10} 11 12// Called when the game starts or when spawned 13void AMaterialShowcaseActor::BeginPlay() 14{ 15 Super::BeginPlay(); 16} 17 18// Called every frame 19void AMaterialShowcaseActor::Tick(float DeltaTime) 20{ 21 Super::Tick(DeltaTime); 22} 可以看到，模板代码中包含一些奇奇怪怪的宏与类（比如 GENERATED_BODY()、AActor\u0026hellip;\u0026hellip;），这些可以参见之前写的 UE 反射系统 与 UE Gameplay Framework（一）。总之，关于这些东西是什么以及他们的作用，这里不再赘述\n修改 .h 和 .cpp 文件 在 MaterialShowcaseActor.h 之中添加不同的属性与之后会用到的函数\n1#pragma once 2 3#include \u0026#34;CoreMinimal.h\u0026#34; 4#include \u0026#34;GameFramework/Actor.h\u0026#34; 5#include \u0026#34;MaterialShowcaseActor.generated.h\u0026#34; 6 7UCLASS() 8class UE5_MATERIAL_LAB_API AMaterialShowcaseActor : public AActor 9{ 10 GENERATED_BODY() 11 12public:\t13 // Sets default values for this actor\u0026#39;s properties 14 AMaterialShowcaseActor(); 15 16protected: 17 // Called when the game starts or when spawned 18 virtual void BeginPlay() override; 19 20/* ------------ 新增部分 ------------ */ 21 // Actor 被放置、Details 面板中的参数被修改时调用 22 virtual void OnConstruction(const FTransform\u0026amp; Transform) override; 23/* --------------------------------- */ 24 25public:\t26 // Called every frame 27 virtual void Tick(float DeltaTime) override; 28 29/* ------------ 新增部分 ------------ */ 30public: 31 // 模型组件：编辑器中可见，但不可替换；类型为 Components 32 UPROPERTY(VisibleAnywhere, Category = \u0026#34;Components\u0026#34;) 33 TObjectPtr\u0026lt;UStaticMeshComponent\u0026gt; MeshComponent; 34 35 // 原始材质引用：可编辑；类型为 Material 36 UPROPERTY(EditAnywhere, Category = \u0026#34;Material\u0026#34;) 37 TObjectPtr\u0026lt;UMaterialInterface\u0026gt; SourceMaterial; 38 39 // 基础色，后面的几个参数都可在 Details 面板中编辑 40 UPROPERTY(EditAnywhere, Category = \u0026#34;Material\u0026#34;) 41 FLinearColor BaseColor = FLinearColor::White; 42 43 // 粗糙度 44 UPROPERTY(EditAnywhere, Category = \u0026#34;Material\u0026#34;, meta = (ClampMin = \u0026#34;0.0\u0026#34;, ClampMax = \u0026#34;1.0\u0026#34;)) 45 float Roughness = 0.5f; 46 47 // 自发光强度 48 UPROPERTY(EditAnywhere, Category = \u0026#34;Material\u0026#34;, meta = (ClampMin = \u0026#34;0.0\u0026#34;)) 49 float EmissiveStrength = 0.0f; 50 51 // 动态的材质实例 52 // 这是因为我们不希望在运行时对源材质进行修改（会导致整个材质类的重新编译，同时也会影响所有使用这个材质的物体） 53 // 以此需要在运行时创建一个动态实例，在这之上的修改只会作用于当前 Actor 54 UPROPERTY() 55 TObjectPtr\u0026lt;UMaterialInstanceDynamic\u0026gt; MaterialInstance; 56 57 // 一个辅助函数，用于将 C++ 变量同步到材质 58 void UpdateMaterialInstance(); 59/* --------------------------------- */ 60}; 在 MaterialShowcaseActor.cpp 中对之前声明的函数进行定义\n1#include \u0026#34;MaterialShowcaseActor.h\u0026#34; 2#include \u0026#34;Components/StaticMeshComponent.h\u0026#34; 3#include \u0026#34;Materials/MaterialInstanceDynamic.h\u0026#34; 4 5// Sets default values 6AMaterialShowcaseActor::AMaterialShowcaseActor() 7{ 8 // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don\u0026#39;t need it. 9 PrimaryActorTick.bCanEverTick = true; 10 11/* ------------ 新增部分 ------------ */ 12 // Actor 本身只是一个容器，真正的显示则是由一个个的组件所实现 13 // 这里则是创建了一个 StaticMeshComponent 14 MeshComponent = CreateDefaultSubobject\u0026lt;UStaticMeshComponent\u0026gt;(TEXT(\u0026#34;MeshComponent\u0026#34;)); 15 // 将这个组件设置为根组件，之后的旋转、缩放等操作都以此为基础进行 16 RootComponent = MeshComponent; 17/* --------------------------------- */ 18} 19 20// Called when the game starts or when spawned 21void AMaterialShowcaseActor::BeginPlay() 22{ 23 Super::BeginPlay(); 24 25/* ------------ 新增部分 ------------ */ 26 // 如果没有在 Details 面板中填入材质，就不执行，避免空指针错误 27 if (SourceMaterial) 28 { 29 // 根据 SourceMaterial 创建一份材质实例 30 MaterialInstance = UMaterialInstanceDynamic::Create(SourceMaterial, this); 31 // 并将这个材质实例绑定到 0 号材质槽中 32 MeshComponent-\u0026gt;SetMaterial(0, MaterialInstance); 33 // 将 C++ 中定义的参数写入材质实例 34 UpdateMaterialInstance(); 35 } 36} 37 38// 这个函数用来创建动态材质与更新参数 39// 因此即使没有运行也可以在预览窗口中实时看到材质变化 40void AMaterialShowcaseActor::OnConstruction(const FTransform\u0026amp; Transform) 41{ 42 Super::OnConstruction(Transform); 43 44 if (SourceMaterial) 45 { 46 MaterialInstance = UMaterialInstanceDynamic::Create(SourceMaterial, this); 47 MeshComponent-\u0026gt;SetMaterial(0, MaterialInstance); 48 UpdateMaterialInstance(); 49 } 50} 51 52void AMaterialShowcaseActor::UpdateMaterialInstance() 53{ 54 // 如果连材质实例都不存在，自然也就没必要更新，直接退出即可 55 if (!MaterialInstance) 56 { 57 return; 58 } 59 60 // 这里是真正将 C++ 与材质连接在一起的地方 61 // 需要注意的是，这里的字符串必须与材质蓝图中的参数名完全一致 62 // 如果不一致，编译器倒是不会报错，但调整参数也不会有效果 63 MaterialInstance-\u0026gt;SetVectorParameterValue(TEXT(\u0026#34;BaseColor\u0026#34;), BaseColor); 64 MaterialInstance-\u0026gt;SetScalarParameterValue(TEXT(\u0026#34;Roughness\u0026#34;), Roughness); 65 MaterialInstance-\u0026gt;SetScalarParameterValue(TEXT(\u0026#34;EmissiveStrength\u0026#34;), EmissiveStrength); 66} 67/* --------------------------------- */ 68 69// Called every frame 70void AMaterialShowcaseActor::Tick(float DeltaTime) 71{ 72 Super::Tick(DeltaTime); 73} 创建材质 在写代码时就已经可以知道，通过 C++ 类所创建的 Actor，必须得有一个材质进行承接，不然代码中的 if 语句会直接将流程跳过。\n在 UE 的内容浏览器中创建一个名为“M_ShowCase”的材质，进入蓝图后创建三个参数——BaseColor、Roughness、EmissiveStrength 并进行如图连接：\n编译 这里既可以直接在 IDE 中进行编译，同时也可以点击 UE 编辑器下方栏中的编译按钮，之后在内容浏览器中就能找到“C++ 类 / YourProject / Public / MaterialShowcaseActor”\n设置 Actor 将此 Actor 拖入场景中，可以看到 Actor 是空的，什么也没有，这是因为 Actor 还没有绑定组件，在右侧的 Detail 栏中随便选一个 Static Mesh 即可，之后再把上一步创建的材质拖进材质槽位。此时如果对参数进行修改，正常情况下应该是会作用到 Actor 上的\n总结： 至此，我们已经打通了 C++ 与引擎通信的第一层：\nC++ class\n-\u0026gt; UE 反射系统\n-\u0026gt; Editor Details 面板\n-\u0026gt; Actor 实例\n-\u0026gt; Component\n-\u0026gt; Material asset\n-\u0026gt; Dynamic Material Instance\n-\u0026gt; Runtime parameter update\n","permalink":"https://jiat-t.github.io/ue/cpp-in-ue5/","summary":"\u003cp\u003e通过这个小练习，完成后应该了解到的是：\u003cstrong\u003eC++ 负责定义逻辑与接口，而 UE 编辑器则负责创建与摆放对象、调整参数以及最终 Shader 的实现。\u003cstrong\u003e同时，也能够\u003c/strong\u003e学习 UE C++ 怎么把变量暴露到编辑器、怎么创建组件、怎么引用资产、怎么生成动态材质实例、怎么给蓝图提供接口\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这个练习也非常简单：通过 C++ 创建一个 Actor 对象，让它出现在 UE 的编辑器里，并且我们能够通过编辑器的 Details 面板来调整这个 Actor 的颜色、粗糙度、自发光强度等等\u003c/p\u003e\n\u003cp\u003e注：这里我使用的是 UE5.6 版本\u003c/p\u003e\n\u003ch2 id=\"创建一个-c-类\"\u003e创建一个 C++ 类\u003c/h2\u003e\n\u003cp\u003e点击编辑器上方的 \u003cstrong\u003e工具\u003c/strong\u003e -\u0026gt; \u003cstrong\u003e新建 C++ 类\u003c/strong\u003e -\u0026gt; \u003cstrong\u003eActor\u003c/strong\u003e，命名为“MaterialShowcaseActor”，之后创建\u003c/p\u003e\n\u003cp\u003e此时 UE 会自动生成两个文件：MaterialShowcaseActor.h 和 MaterialShowcaseActor.cpp，与大多数类一样，前者用于声明，后者用于定义，模板如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 1\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#pragma once\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 2\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026#34;CoreMinimal.h\u0026#34;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 3\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026#34;GameFramework/Actor.h\u0026#34;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 4\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026#34;MaterialShowcaseActor.generated.h\u0026#34;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 5\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 6\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eUCLASS\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 7\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 注意这里的类名是项目的名字 + \u0026#34;_API\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 8\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eUE5_MATERIAL_LAB_API\u003c/span\u003e \u003cspan class=\"nl\"\u003eAMaterialShowcaseActor\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"k\"\u003epublic\u003c/span\u003e \u003cspan class=\"n\"\u003eAActor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 9\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e10\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eGENERATED_BODY\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e11\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e12\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e13\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Sets default values for this actor\u0026#39;s properties\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e14\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eAMaterialShowcaseActor\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e15\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e16\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eprotected\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e17\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Called when the game starts or when spawned\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e18\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eBeginPlay\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"k\"\u003eoverride\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e19\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e20\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003epublic\u003c/span\u003e\u003cspan class=\"o\"\u003e:\u003c/span\u003e\t\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e21\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Called every frame\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e22\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003evirtual\u003c/span\u003e \u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eTick\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003efloat\u003c/span\u003e \u003cspan class=\"n\"\u003eDeltaTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"k\"\u003eoverride\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e23\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 1\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026#34;MaterialShowcaseActor.h\u0026#34;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 2\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026#34;Components/StaticMeshComponent.h\u0026#34;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 3\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026#34;Materials/MaterialInstanceDynamic.h\u0026#34;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 4\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 5\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Sets default values\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 6\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eAMaterialShowcaseActor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eAMaterialShowcaseActor\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 7\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 8\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don\u0026#39;t need it.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 9\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003ePrimaryActorTick\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ebCanEverTick\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"nb\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e10\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e11\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e12\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Called when the game starts or when spawned\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e13\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAMaterialShowcaseActor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginPlay\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e14\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e15\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eSuper\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eBeginPlay\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e16\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e17\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e18\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Called every frame\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e19\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAMaterialShowcaseActor\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eTick\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003efloat\u003c/span\u003e \u003cspan class=\"n\"\u003eDeltaTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e20\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e21\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eSuper\u003c/span\u003e\u003cspan class=\"o\"\u003e::\u003c/span\u003e\u003cspan class=\"n\"\u003eTick\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDeltaTime\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e22\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e可以看到，模板代码中包含一些奇奇怪怪的宏与类（比如 GENERATED_BODY()、AActor\u0026hellip;\u0026hellip;），这些可以参见之前写的 \u003ca href=\"https://jiat-t.github.io/ue/reflection-system/\"\u003eUE 反射系统\u003c/a\u003e 与 \u003ca href=\"https://jiat-t.github.io/ue/gameplay-framework1/\"\u003eUE Gameplay Framework（一）\u003c/a\u003e。总之，关于这些东西是什么以及他们的作用，这里不再赘述\u003c/p\u003e","title":"C++ 管理引擎资产的简单练习"},{"content":"Data Race（数据竞争） 和 Race Condition（竞态条件） 经常被混淆，但在严格的并发编程语义下，它们并不是同一个概念，不能画等号\n简单来说：\nData Race（数据竞争） 是一个关于内存访问的底层物理问题 Race Condition（竞态条件） 是一个关于执行顺序与业务逻辑的高层逻辑问题 1. 什么是 Data Race（数据竞争） 在 C++ 标准中，Data Race 有着极其严格的定义。当满足以下三个条件时，就一定会发生数据竞争：\n两个或多个线程同时（并发）访问同一个内存地址 至少有一个访问是写操作（Write） 这些访问没有使用任何同步机制（比如没有加互斥锁 std::mutex，也没有使用原子变量 std::atomic） 后果：未定义行为 在 C++ 规范里，只要代码中存在 Data Race，整个程序就会直接沦为未定义行为（UB）。 编译器可能会把代码优化得面目全非，导致不可预知的崩溃或极其诡异的 Bug\n2. 什么是 Race Condition（竞态条件） Race Condition 指的是程序的输出依赖于多个线程执行的相对顺序（时序）。如果由于调度的随机性，导致线程执行顺序不对，程序的逻辑就会出错。\n它通常发生在业务层。即使内存访问是安全的（比如每一步都加了锁，不会发生 Data Race），但只要多个操作之间的顺序出了错，依然会触发 Race Condition\n3. 对比：有 Race Condition 但没有 Data Race 假设我们用一个线程安全的并发链表（所有操作都加了锁）来写一段多线程代码：\n1// 假设这个并发链表内部是互斥锁保护的，没有 Data Race 2ThreadSafeList\u0026lt;int\u0026gt; myList; 3 4// 线程 A 和 线程 B 都在执行这段代码 5if (!myList.contains(42)) { 6 // 隐患：在检查和插入之间，当前线程可能会被挂起 7 myList.insert(42); 8} 为什么它没有 Data Race 因为 contains 和 insert 内部都正确地加了锁，多线程并发调用它们时，内存访问是完全安全的，绝不会触发 C++ 的未定义行为\n为什么它有 Race Condition（竞态条件） 假设链表初始没有 42：\n线程 A 检查 contains(42)，返回 false 此时时间片到了，线程 A 被挂起，线程 B 切进来 线程 B 检查 contains(42)，返回 false 线程 B 执行 insert(42) 线程 A 被唤醒，继续执行 insert(42) 结果： 链表里被重复插入了两个 42。由于执行时序的先后破坏了“不包含才插入”的业务逻辑，这就是 Race Condition\n4. 总结与直观关系 有 Data Race，无 Race Condition： 很少见，比如两个线程在没有同步的情况下同时往一个全局变量写同一个固定的值（如 x = 42）。无论谁先写，结果都是 42，业务逻辑没错，但由于没加锁，它依然是 Data Race（C++ 判定为 UB）。 有 Race Condition，无 Data Race： 上文提到的并发链表例子。每一步都安全加锁，但宏观时序混乱导致逻辑错误。 两者都有（最常见）： 两个线程在没有锁保护的情况下，同时对一个普通的 int count 变量执行 count++。这既破坏了内存访问安全（Data Race），又导致最终计数不对（Race Condition）。 ","permalink":"https://jiat-t.github.io/cpp/data-race-and-race-condition/","summary":"区分 C++ 并发语境下 Data Race 与 Race Condition 的含义和关系。","title":"Data Race 和 Race Condition"},{"content":"前向渲染（Forward Rendering） 前向渲染走的就是传统的光栅化渲染管线：应用阶段 -\u0026gt; 几何阶段 -\u0026gt; 光栅化阶段 -\u0026gt; 像素处理阶段\n也就是说，对于每一个物体，都需要对每一个光源进行一次计算，伪代码如下\n1for each object 2{ 3 // 1. 几何阶段：顶点着色器处理物体的顶点 4 vertexShader(object); 5 6 for each primitive of the object 7 { 8 // 2. 光栅化：把三角形打散成一个个像素点（Fragments） 9 fragments = rasterize(primitive); 10 11 for each fragment in fragments 12 { 13 // 初始化最终的像素颜色 14 float4 finalColor = ambient; 15 16 // 3. 核心：前向渲染会对每个片段遍历所有光源 17 for each light 18 { 19 finalColor += pixelShader(fragment, light); 20 } 21 22 // 4. 逐片段操作：深度测试与写入 23 if (depthTest passed) 24 { 25 writeToColorBuffer(fragment.screenX, fragment.screenY, finalColor); 26 writeToDepthBuffer(fragment.screenX, fragment.screenY, fragment.depth); 27 } else 28 { 29 discard; // 深度测试失败，丢弃 30 } 31 } 32 } 33} 好处：天然支持 MSAA 以及透明物体的渲染，不需要额外的 Pass 或者 G-Buffer，节省带宽\n缺点：\n从伪代码中就可以看出，如果场景中存在大量光源与大量物体，循环就会被执行非常多次，其复杂度为 O(n * m)，其中 n 是物体数量，m 是光源数量 由于深度测试位于片元着色器之后，所以可能会产生非常严重的 OverDraw，即一个像素被绘制多次，这在 pixelShader 的计算量偏大的情况下（比如 BSDF 的计算）是不可接受的——产生了非常严重的性能浪费 延迟渲染（Deferred Rendering） 核心思想是：****先确定可见性，再进行光照计算\nG-Buffer 延迟渲染得以实现的基础是 G-Buffer（几何缓冲），在实际的图形 API 中 G-Buffer往往不是一张单一的超大纹理，而是通过 GPU 的 MRT（Multi-Render Target）技术，在一个 Pass 里将不同的信息（比如 Albedo、Normal\u0026hellip;\u0026hellip;）写入多张 RT 中，一种常见的布局如下：\nRender Target Format R 通道 (8 bit) G 通道 (8 bit) B 通道 (8 bit) A 通道 (8 bit) RT0 (Scene Color) RGBA8 固有色 (Albedo.r) 固有色 (Albedo.g) 固有色 (Albedo.b) 材质 ID (Material ID) RT1 (Normal) RGBA8 或 RGBA16F 法线X(Normal.x) 法线 Y(Normal.y) 法线 Z(Normal.z) 间接光倍率 / 其它 RT2 (Metallic/Roughness) RGBA8 金属度(Metallic) 粗糙度 (Roughness) 环境光遮蔽 (AO) 阴影遮蔽 (Shading Model) RT3 (Velocity/Custom) RGBA8 自发光 (Emissive) 或 屏幕空间速度矢量 (Motion Vector)，用于 TAA 抗锯齿和动态模糊 Depth-Stencil Buffer D24S8 / D32F 深度值 (Depth)：不占用 MRT 的 RT 槽位，是由硬件深度缓冲区直接记录的。 所有的 RT 合起来组成 G-Buffer，相应地，更多的 RT 意味着需要更高的显存带宽，这也是延迟渲染的主要性能瓶颈之一\n延迟渲染的过程 不同于前向渲染的单 Pass（通常情况下只有一个 Pass），延迟渲染使用到了两个Pass，分别是 Geometry Pass 和 Light Pass。前者负责填充 G-Buffer，后者负责执行着色\nGeometry Pass ： 几何阶段不进行任何光照计算，而是遍历场景中的每一个物体，并执行深度测试，确定了最终呈现在平面上的像素之后，再在 Pixel Shader 中将这个像素的信息逐个填入对应的 RT 中\nMRT：多重渲染目标技术，使得我们可以在一个 Pass 中就将所需的所有信息写入多张 RT 中，而不是使用多个 Pass，每个 Pass 只写一张 RT\nLight Pass： 此时几何信息已经全都被我们拿到了，所以只需要在屏幕空间无脑地根据 G-Buffer 进行计算即可；同时也正因为我们拿到的都是最后一定会呈现在屏幕上的像素的信息，所以完全不会出现“一个像素被绘制多次”的情况，大大减少了 OverDraw\n伪代码如下：\n1// ==================== 1. 几何阶段 ==================== 2for each object in scene 3{ 4 for each fragment of object 5 { 6 if (depthTest passed) 7 { 8 // 不计算光照，只填充几何属性 9 writeRT; 10 } 11 } 12} 13 14// ==================== 2. 光照阶段 ==================== 15// 绘制一个覆盖全屏幕的巨型三角形或矩形 16for each pixel on screen 17{ 18 // 采样 G-Buffer 得到该像素的真实物理属性 19 sample G-Buffer; 20 // 核心：在这里才遍历光源，没有 Overdraw 浪费 21 for each light in scene 22 { 23 pixelShader 24 } 25} 好处：\n有效规避了 Forward Rendeirng 的 OverDraw 现象，大大提高了性能； 针对多光源场景，Forward Rendeirng 必须在 Pixel Shader 中一个一个地进行遍历，而 Deferred Rendering 由于第一个 Pass 已经拿到了所有的几何信息，所以随着光源数量的上升，复杂度只会呈线性增加，而非与物体数量相关联——此时复杂度为 O(r * m)，其中 r 为屏幕分辨率，m 为光源数量 缺点：\n不支持 MSAA：MSAA 的核心是在 Pixel Shader 中将片元所覆盖的像素进行划分，再根据所覆盖的子像素与所有子像素的比例决定“柔化系数”。但是在 Deferred Rendering 中，光栅化与 Pixel Shader 位于两个不同的 Pass 中，Pixel shader 能拿到的只有上一个 Pass 传下来的信息，即 G-Buffer 纹理中每个像素对应的标量 / 向量信息，而对于“片元覆盖了多少子像素”这类信息，G-Buffer 中并没有记录。但也并不是说延迟渲染中完全不能实现 MSAA，只是如果这样做的话，需要根据 MSAA 的倍率额外存储更多的 RT（以 4x MSAA 为例，需要用 4 倍的 G-Buffer 尺寸来存储子像素），而这会导致显存带宽的崩溃 不支持半透明物体：G-Buffer 的每一个像素只能存储一个表面的信息，而对于半透明物体，一个像素可能需要用到多个表面的信息。解决方法是先对不透明物体进行一次延迟渲染，然后再对透明物体补一次前向渲染 带宽占用极高：Geometry Pass 需要写入大量信息进入 G-Buffer，Light Pass 又需要读出这些巨量信息，这对显存以及显存的带宽是极大的负担 基于瓦片的渲染（Tile-Based Rendering，TBR） 前两种渲染路径都属于 IMR（Immediate Mode Rendering，立即模式渲染） 的范畴，它的运作方式是：\n拿到三角形 -\u0026gt; 顶点着色器 -\u0026gt; 光栅化 -\u0026gt; 片元着色器 \u0026lt;-\u0026gt; 读写 VRAM\n也就是说，IMR 的策略是拿到三角形就直接丢进显存进行渲染，完全不管这会占用多少带宽。而之所以可以这么做，完全是因为桌面端的 GPU 性能支持这种力大砖飞的方法——拥有独立显存，同时总线功耗极高，动辄就是上百瓦，完全不怕疯狂读取显存\n但是移动端就完全不一样了，手机上没有独立的显存，CPU 与 GPU 需要共享一块系统内存，同时这块内存的带宽也极低，如果还是像桌面端那样频繁的进行读取操作，手机就会因为带宽堵塞而发烫，很快就会没电\nTBR 的策略则是将屏幕画面划分为一块一块的 tile（比如 16 x 16 的块），然后对整个 tile 进行渲染，以达到节省带宽的目的（因为一次将多个三角形打包进入显卡）\n虽然移动端的 GPU 没有独立的显存，但是 GPU 上还是存在着一块高速的 L1、L2 缓存，因此如果可以将所有计算都一次性在这片内存上完成，就可以省下大笔带宽，这也是 TBR 的核心思想\n具体步骤 几何阶段： GPU 拿到几何体信息后，先执行顶点着色器，得到顶点的物理位置，并将其归类进不同的 tile\n像素阶段： 当一帧结束，此时 GPU 必须要进行渲染了，它才会执行光栅化与片元着色器等后续流程，根据高速缓存中存储的信息，将每一个 tile 内部的三角形一次性画完，然后将计算好的 Buffer 写回系统内存。在此过程中，由于 Depth-Buffer 与 Color-Buffer 始终没有离开过 GPU，因此完全不存在带宽的高占用情况\n既然 TBR 可以减少带宽占用，为什么桌面端不采用这种方式？\n核心瓶颈不同：桌面端 GPU 不在乎这些带宽的占用，因为性能高到完全足以应付，桌面端注重的是极致的吞吐量与运行速度，而 TBR 需要等待每一帧刷新之后才执行光照计算，效率不如 IMR 几何数量过载：3A 游戏中，每一帧动辄就是成百上千万的面数，如果在 GPU 端对这些几何体一一记载的话，不仅显存空间不够，还会导致渲染流水线的断层，即 GPU 必须先把海量的顶点进行归类之后，才能继续进行下一步的计算 无法用于无序渲染：诸如 Compute Shader、GPU Culling、Ray-Tracing 都是完全无序的、动态的，或者需要随机访问全场景的。而 TBR 一旦遇到这种需要跨 tile 访问的情况，就要将已经处理好的数据重新刷新会显存，导致 TBR 的优势荡然无存 基于瓦片的延迟渲染（Tile-Based Deferred Rendering，TBDR） 相对于前面说过的延迟渲染，TBDR 属于移动端 GPU 自带的硬件层面的能力，它的内部增加了一个强悍的硬件剔除单元——HSR（Hidden Surface Removal，隐面剔除）\n具体步骤 几何与分块： 这一步与 TBR 完全一致，CPU 发来绘制命令，GPU 执行 VS 并将三角形分块存储\n光栅化与 HSR 测试： 在每一个 tile 中，硬件会对每一个三角形进行光栅化，之后不是直接执行片元着色器，而是由 HSR 进行深度测试\n执行片元着色器并写回： 此时，**片上高速缓存（On-Chip Buffer）**里留下的，一定是最终能被眼睛看到的、唯一的那层像素，接着 GPU 才会对这些像素执行光照计算。最后将所有计算完毕的像素，一次性打包回系统内存\n优点：由于 HSR 在硬件层面的深度测试，因此基本不存在 OverDraw 的情况\n缺点：Alpha Test 不友好，一旦遇到 Alpha Test 的物体，TBDR 会被迫强行打断 HSR 流水线。硬件必须停下来，先把这个草叶物体之前的所有东西画像素、刷新深度，或者挂起等待 Pixel Shader 执行完返回结果。这就导致 TBDR 瞬间退化，甚至比普通 TBR 还要慢\n小结 这里只是泛泛地谈了下移动端与桌面端常见的渲染策略，以这些方法为主流还可以拓展出更多更加优秀的渲染路径，不过就这样吧，下面引用的文章中已经有了详细的记载，不知道的直接去查即可\n【Reference】\nhttps://zhuanlan.zhihu.com/p/381682257\nhttps://zhuanlan.zhihu.com/p/259760974\n","permalink":"https://jiat-t.github.io/graphics/rendering-paths/","summary":"\u003ch2 id=\"前向渲染forward-rendering\"\u003e前向渲染（Forward Rendering）\u003c/h2\u003e\n\u003cp\u003e前向渲染走的就是\u003cstrong\u003e传统的光栅化渲染管线\u003c/strong\u003e：\u003cu\u003e应用阶段 -\u0026gt; 几何阶段 -\u0026gt; 光栅化阶段 -\u0026gt; 像素处理阶段\u003c/u\u003e\u003c/p\u003e\n\u003cp\u003e也就是说，对于每一个物体，都需要对每一个光源进行一次计算，伪代码如下\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 1\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eeach\u003c/span\u003e \u003cspan class=\"n\"\u003eobject\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 2\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 3\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e// 1. 几何阶段：顶点着色器处理物体的顶点\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 4\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003evertexShader\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eobject\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 5\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 6\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eeach\u003c/span\u003e \u003cspan class=\"n\"\u003eprimitive\u003c/span\u003e \u003cspan class=\"n\"\u003eof\u003c/span\u003e \u003cspan class=\"n\"\u003ethe\u003c/span\u003e \u003cspan class=\"n\"\u003eobject\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 7\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 8\u003c/span\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"c1\"\u003e// 2. 光栅化：把三角形打散成一个个像素点（Fragments）\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e 9\u003c/span\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003efragments\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erasterize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eprimitive\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e10\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e11\u003c/span\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eeach\u003c/span\u003e \u003cspan class=\"n\"\u003efragment\u003c/span\u003e \u003cspan class=\"n\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003efragments\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e12\u003c/span\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e13\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// 初始化最终的像素颜色\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e14\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"n\"\u003efloat4\u003c/span\u003e \u003cspan class=\"n\"\u003efinalColor\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eambient\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e15\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e16\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// 3. 核心：前向渲染会对每个片段遍历所有光源\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e17\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003eeach\u003c/span\u003e \u003cspan class=\"n\"\u003elight\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e18\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e19\u003c/span\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003efinalColor\u003c/span\u003e \u003cspan class=\"o\"\u003e+=\u003c/span\u003e \u003cspan class=\"n\"\u003epixelShader\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efragment\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003elight\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e20\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e21\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e22\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"c1\"\u003e// 4. 逐片段操作：深度测试与写入\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e23\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edepthTest\u003c/span\u003e \u003cspan class=\"n\"\u003epassed\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e24\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e25\u003c/span\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ewriteToColorBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efragment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003escreenX\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efragment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003escreenY\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efinalColor\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e26\u003c/span\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ewriteToDepthBuffer\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efragment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003escreenX\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efragment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003escreenY\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003efragment\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edepth\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e27\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"k\"\u003eelse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e28\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e29\u003c/span\u003e\u003cspan class=\"cl\"\u003e                \u003cspan class=\"n\"\u003ediscard\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 深度测试失败，丢弃\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e30\u003c/span\u003e\u003cspan class=\"cl\"\u003e            \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e31\u003c/span\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e32\u003c/span\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e33\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e好处\u003c/strong\u003e：天然支持 MSAA 以及透明物体的渲染，不需要额外的 Pass 或者 G-Buffer，节省带宽\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺点\u003c/strong\u003e：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e从伪代码中就可以看出，如果场景中\u003cstrong\u003e存在大量光源与大量物体\u003c/strong\u003e，循环就会被执行非常多次，其复杂度为 \u003cstrong\u003eO(n * m)\u003c/strong\u003e，其中 n 是物体数量，m 是光源数量\u003c/li\u003e\n\u003cli\u003e由于深度测试位于片元着色器之后，所以可能会产生非常严重的 \u003cstrong\u003eOverDraw\u003c/strong\u003e，即一个像素被绘制多次，这在 pixelShader 的计算量偏大的情况下（比如 \u003cstrong\u003eBSDF\u003c/strong\u003e 的计算）是不可接受的——产生了非常严重的\u003cstrong\u003e性能浪费\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"延迟渲染deferred-rendering\"\u003e延迟渲染（Deferred Rendering）\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e核心思想是：****\u003cu\u003e先确定可见性，再进行光照计算\u003c/u\u003e\u003c/strong\u003e\u003c/p\u003e","title":"Rendering Paths"},{"content":"在早期计算光照贡献时，用到的大多都是 Bllin-Phong 模型，但这一模型本身就是经验性的，存在着能量不守恒等问题，而基于辐射度量学的一套模型正能够几乎完美的模拟真实世界中的物理光照\n首先需要了解的是**“立体角（Solid Angle）”**，它的定义是：（摘自维基百科）：\n以观测点为球心，构造一个单位球面；任意物体投影到该单位球面上的投影面积，即为该物体相对于该观测点的立体角。因此，立体角是单位球面上的一块面积，这和“平面角是单位圆上的一段弧长”类似。\n立体角的单位是球面度（steradians, sr），微立体角可以表示为 $d\\omega = \\sin\\theta d\\theta d\\phi$\n辐射能 (Radiant Energy, $Q$) 含义：顾名思义，辐射能就是电磁波（光）携带的能量总和，单位为焦耳（$J$）\n物理意义：光源发射多少能量，或物体表面接受了多少能量\n在渲染中的应用：用的不多（应该？），因为辐射能代表的是一段时间内的能量总和（类似于机械运动中的总功），但是实际中需要用到的往往是某个稳定状态下的能量\n辐射通量 / 辐射功率 (Radiant Flux / Power, $\\Phi$) 含义：单位时间内的辐射能，所以也辐射通量也叫做辐射功率，单位是瓦特（$W$）\n公式：$\\Phi = \\frac{dQ}{dt}$\n在渲染中的应用：通常用来描述光源的整体亮度，比如“灯泡的亮度是 10w”\n辐射强度 (Radiant Intensity, $I$) 含义：单位立体角内的辐射通量，单位是瓦特每球面度（$W/sr$）\n公式：$I = \\frac{d\\Phi}{d\\omega}$\n在渲染中的应用：通常用来定义点光源或方向光在某个方向上的能量分布\n辐照度 (Irradiance, $E$) 含义：单位面积上接收到的辐射通量，单位是瓦特每平方米（$W/m^2$）\n公式：$E = \\frac{d\\Phi}{dA}$\n在渲染中的应用：这是光照计算中一个非常关键的量，当一束光垂直入射时，光线照射到的面积恰好就是光线的宽度，但是当光斜着打到物体表面时，照射到的面积会随着入射角的 cosine 值增大而减小，这就是兰伯特余弦定律的物理来源：$E = \\frac{\\Phi}{A} \\cos\\theta$\n辐射率 / 辐射亮度 (Radiance, $L$) 这是最最最最最重要的一条概念！！！！！！！！\n含义：单位立体角、单位投影面积上的辐射通量，单位是 $W \\cdot sr^{-1} \\cdot m^{-2}$\n公式：$L = \\frac{d^2\\Phi}{d\\omega dA \\cos\\theta}$\n在渲染中的应用：用于描述一条射线所携带的能量\n总结！ 可以把光想象成水流：\n$Q $(辐射能) = 水的总量。 $\\Phi$(辐射通量) = 水流的速度（每秒多少水） $I$(辐射强度) = 水管喷头在某个特定方向上喷出水流的猛烈程度 $E$(辐照度) = 地面上某一块砖被水淋湿的程度 $L$(辐射率) = 在空中飞行的一滴水珠，沿着特定轨迹砸在特定的一小块砖上的能量 来源：语雀原笔记\nReference https://zhuanlan.zhihu.com/p/145410416 ","permalink":"https://jiat-t.github.io/graphics/%E8%BE%90%E5%B0%84%E5%BA%A6%E9%87%8F%E5%AD%A6/","summary":"整理辐射度量学中的 Radiant Energy、Radiant Flux、Radiant Intensity、Irradiance 和 Radiance。","title":"辐射度量学"},{"content":"在 Vulkan 中，核心的队列类型主要有三种。它们负责将 CPU 提交的命令缓冲区分发到 GPU 的不同硬件单元上执行。\nVulkan 通过队列族（Queue Family）来管理这些队列。一个队列族可以支持一种或多种队列类型，最核心的三种队列类型如下。\n1. 图形队列（Graphics Queue） 核心职能：负责处理所有传统 3D / 2D 图形渲染管线任务。 常见操作：绑定顶点缓冲区、设置管线状态、绘制命令、视口设置、光栅化、片元着色器执行等。 特点：这是游戏和图形应用中最常用的队列。在大多数独立显卡上，图形队列族通常不只处理渲染流程中的步骤，也会隐式包含计算和传输功能。 2. 计算队列（Compute Queue） 核心职能：负责执行计算着色器（Compute Shader），即 GPGPU（General-Purpose GPU）任务。 常见操作：并行计算、物理模拟、图像后处理、粒子系统更新，以及 AI / 机器学习推断等。 特点：它不需要走传统的光栅化、混合等图形管线。许多现代显卡提供异步计算（Async Compute）能力，允许独立的计算队列与图形队列并行执行，从而更充分地利用 GPU 硬件。 3. 传输队列（Transfer Queue） 核心职能：专门负责内存数据的复制和传输。 常见操作：显存与系统内存之间的拷贝，将 staging buffer（暂存缓冲区）中的数据上传到高性能的本地显存中。 特点：传输队列通常映射到 GPU 的 DMA（Direct Memory Access，直接内存访问）引擎。使用独立的传输队列可以在后台上传纹理、模型网格等资源，而不阻塞主图形队列的渲染，从而实现更平滑的资源加载。 补充：第四种常见类型 除了上述三大核心队列，标准 Vulkan 硬件中还经常出现第四种队列类型。\n稀疏绑定队列（Sparse Binding Queue）：标记为 VK_QUEUE_SPARSE_BINDING_BIT。它用于管理稀疏资源，比如大型虚拟纹理，允许将虚拟内存页动态绑定到实际的物理显存分配上。 ","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/queue-categories/","summary":"整理 Vulkan 中常见队列类型：Graphics、Compute、Transfer 以及 Sparse Binding Queue。","title":"Queue Categories"},{"content":"\n预处理 这一阶段主要处理以 # 开头的代码段，不进行语法分析，更多的是文本替换。\n宏展开：将所有 #define 的宏定义在调用处展开。 头文件包含：处理 #include 指令，将所有引用的文件，比如 .h、.hpp，完整复制到当前文件。 条件编译：根据 #ifdef、#ifndef、#endif 决定保留或删除哪些代码。 删除注释：删除单行注释 // 与多行注释 /* */。 编译 这一阶段会将预处理后的文本编译为与 CPU 架构紧密相关的汇编语言，同时也是最复杂的一步。\n语法分析：检查代码是否存在语法错误，比如是不是少了分号、是不是使用了没有定义的变量等。 语义分析：检查类型是否匹配、传入函数的参数是否正确等。 代码优化：编译器会尝试对已有代码进行性能上的优化，比如循环展开、常量折叠等。 生成汇编：确保代码没有错误之后，编译器会将 C++ 这类高级语言翻译为汇编语言指令。 汇编 这一阶段要做的是根据汇编语言指令，一对一地将其翻译为机器能直接识别的二进制码。最终输出的文件是 Object File，在 Windows 中是 .obj 文件，在 Linux 中是 .o 文件。\n注意：此时各个文件仍然是孤立的，所以诸如全局变量、外部函数调用等，在 Object File 中只会使用占位符来表示。不过如果函数的定义与声明都写在 .h 文件中，就不需要占位了。\n链接 这一阶段会将一个或多个 .obj 文件，以及系统或第三方的库文件，比如 .lib 等，链接在一起，最终形成 .exe 文件。\n符号解析：找到当前文件所使用的外部函数，将占位符指向函数在外部 .cpp 文件中的具体实现地址。 重定位：合并所有目标文件的代码段与数据段，并为其分配最终地址。 库引入：对于静态库，直接将其打包进可执行文件；对于动态链接库，比如 .dll，则生成运行时所需的调用信息。 ","permalink":"https://jiat-t.github.io/cpp/cpp-to-exe/","summary":"记录 C++ 源码从预处理、编译、汇编到链接生成可执行文件的基本流程。","title":"C++ 从源码到 exe 的过程"},{"content":"基于辐射度方程的拆分，IBL 主要分为处理漫反射（Diffuse）和镜面反射（Specular）两个核心部分。\n1. Irradiance Map（辐照度图） Irradiance Map 用于解决 PBR 中的漫反射（Diffuse Lambertian）光照部分。\n核心原理 漫反射片元会接收半球方向（Hemisphere）上所有入射光线的贡献。常规计算需要对整个半球进行蒙特卡洛积分：\n$$ I = \\int_{\\Omega} L_i(p, \\omega_i)(\\mathbf{n} \\cdot \\omega_i) d\\omega_i $$在实时渲染中，不可能对每个像素都运行这种高采样率的积分。\n怎么做与存什么 因为漫反射与观察视角（View Direction）无关，只与表面的法线方向（Normal $\\mathbf{n}$）有关。因此，我们可以预先旋转一个半球微表面，计算出每一个可能的法线方向 $\\mathbf{n}$ 所能接收到的总辐射度，然后将这个结果存储在一张新的立方体贴图（Cube Map）中。\n输入： 原始的高动态范围环境贴图（HDR Environment Map）。 采样/存储： 其纹理坐标代表法线方向。当着色器计算漫反射时，直接用片元的法线 $\\mathbf{n}$ 去采样 Irradiance Map，即可在 $O(1)$ 时间内得到环境光对该点的漫反射贡献。 视觉特征： 由于半球积分相当于一个极低通滤波器（Low-pass Filter），Irradiance Map 看起来非常模糊，丢失了所有高频细节，只保留宏观的色彩趋向。这张贴图的每一个像素坐标（u, v）代表一个方向，每一个像素的值（RGB）代表“如果法线朝向这个方向，所能接收到的总辐射度”。 2. Prefiltered Environment Map（预滤波环境贴图） Prefiltered Environment Map 用于解决镜面反射（Specular）部分的第一部分。\n核心原理 Epic Games 在推广微表面模型（Microfacet Model）的 IBL 时，引入了裂项近似（Split-Sum Approximation），将镜面反射的积分项拆分为两部分。第一部分只关注环境光与表面粗糙度的关系：\n$$ \\int_{\\Omega} L_i(p, \\omega_i) d\\omega_i \\approx \\frac{1}{N}\\sum_{i=1}^{N}L_i(p, \\omega_k) $$怎么做与存什么 镜面反射不仅与法线有关，还与反射视角和表面的粗糙度（Roughness）紧密相关。粗糙度决定了反射波瓣（Specular Lobe）的宽窄：表面越光滑，波瓣越窄，反射越清晰；表面越粗糙，波瓣越宽，反射越模糊。\n存储结构： 利用 Mipmaps 机制。一张 Prefiltered Map 包含多个 Mip 级别。 Mip 映射关系： Mip 0： 存储粗糙度 = 0 时的反射，也就是完全镜面反射，接近原始环境贴图。 Mip 1 到 Mip N： 随着 Mip 级别升高，对应存储的粗糙度线性增加，积分采样时使用的微表面分布函数（GGX NDF）波瓣越宽。 采样方式： 运行时，Shader 根据片元的反射向量 $\\mathbf{R}$ 和材质的粗糙度，利用 textureLod 函数去对应的 Mip 级别中进行三线性插值采样。 3. LUT（Look-Up Table / 镜面反射积分查找表） LUT 用于解决镜面反射拆分后的第二部分，也就是环境 BRDF 项。\n核心原理 裂项近似拆出来的第二部分，代表了在特定粗糙度下，不同入射角所带来的能量衰减与菲涅尔效应的整合，包括几何遮蔽 $G$ 项和菲涅尔 $F$ 项的积分：\n$$ \\int_{\\Omega} f_r(\\omega_i, \\omega_o)(\\mathbf{n} \\cdot \\omega_i) d\\omega_i $$这个积分虽然复杂，但它的自变量只有两个：法线与视角夹角的余弦值 $\\cos\\theta_v$，即 $\\mathbf{n} \\cdot \\mathbf{v}$，以及材质的粗糙度（Roughness）。\n怎么做与存什么 由于这两个自变量的范围都在 $[0, 1]$ 之间，因此可以将其预计算并存储在一张二维纹理中，这就是 IBL 的 BRDF LUT。\n纹理坐标： Horizontal（X 轴）/ $U$：$\\mathbf{n} \\cdot \\mathbf{v}$ Vertical（Y 轴）/ $V$：粗糙度（Roughness） 存储通道： 通过数学变换，该积分最后可以化简为 $F_0 \\cdot A + B$ 的形式。 纹理的 R 通道存储比例系数 $A$。 纹理的 G 通道存储偏移系数 $B$。 运行时使用： Shader 用 $\\mathbf{n} \\cdot \\mathbf{v}$ 和粗糙度采样这张二维 LUT 纹理，读出 $A$ 和 $B$，结合材质自身的 $F_0$（基础反射率），通过 $F_0 \\cdot A + B$ 即可恢复出镜面反射 BRDF 的缩放值。 总结：运行时如何组装？ 在 PBR 像素着色器中，IBL 的计算流程极其精简和高效。\n1. 漫反射 $$ \\text{Diffuse} = \\text{TextureCube}(\\text{IrradianceMap}, \\mathbf{n}) \\times \\text{Albedo} $$2. 镜面反射 用反射向量 $\\mathbf{R}$ 和粗糙度采样预过滤贴图：$\\text{LD} = \\text{TextureCubeLod}(\\text{PrefilteredMap}, \\mathbf{R}, \\text{Roughness})$ 用 $\\mathbf{n} \\cdot \\mathbf{v}$ 和粗糙度采样查找表：$(A, B) = \\text{Texture2D}(\\text{BRDF_LUT}, \\mathbf{n} \\cdot \\mathbf{v}, \\text{Roughness})$ 融合结果：$\\text{Specular} = \\text{LD} \\times (F_0 \\times A + B)$ 3. 最终环境光 根据能量守恒（菲涅尔系数 $k_s$ 和漫反射系数 $k_d$）将两部分相加。\n","permalink":"https://jiat-t.github.io/graphics/split-sum-ibl/","summary":"整理 PBR IBL 中 Irradiance Map、Prefiltered Environment Map 与 BRDF LUT 的拆分思路和运行时组合方式。","title":"Split-Sum IBL"},{"content":"在 PBR 框架下，材质的视觉表现主要依赖于微表面（Microfacet）理论和能量守恒定律。为了在渲染引擎中标准化地输入这些物理参数，工业界发展出了两套主流的材质工作流：金属度-粗糙度（Metallic-Roughness，MR 模型）与镜面反射-光泽度（Specular-Glossiness，SG 模型）。\n这两种模型在底层物理数学模型（如 BRDF 的计算公式）上是等价的，它们的核心区别在于输入贴图的数据结构、对能量守恒的约束方式以及显存开销。\n1. 金属度-粗糙度（MR）模型 MR 模型是目前游戏引擎（如 Unreal Engine、Unity）和实时渲染管线中最普遍的标准工作流。\n核心贴图构成：\nBase Color（基础色，RGB）： 包含两层物理含义。对于绝缘体（非金属），它表示漫反射的颜色；对于导体（金属），它表示镜面反射的物理基准值，即 $F_0$。 Metallic（金属度，Grayscale）： 掩码贴图。用于区分材质是金属（值为 1）还是绝缘体（值为 0）。 Roughness（粗糙度，Grayscale）： 描述微表面的粗糙程度。值为 0 表示绝对光滑，值为 1 表示极度粗糙。 2. 镜面反射-光泽度（SG）模型 SG 模型提供了更高的自由度，更接近传统的光照计算分离逻辑，常用于对材质细节有极高要求的离线渲染或特定材质制作。\n核心贴图构成：\nDiffuse（漫反射，RGB）： 仅包含材质的漫反射颜色。如果是纯金属，该贴图对应区域必须为纯黑，因为金属没有漫反射。 Specular（镜面反射，RGB）： 直接定义材质的 $F_0$ 值。无论是金属还是非金属，其镜面反射的颜色和强度都完全由这张贴图控制。 Glossiness（光泽度，Grayscale）： 描述微表面的光滑程度。它是 Roughness 的反义值（Glossiness = 1 - Roughness），值为 1 表示绝对光滑，值为 0 表示极度粗糙。 3. 核心区别对比 特性维度 MR 模型（Metallic-Roughness） SG 模型（Specular-Glossiness） 通道数量 5 个通道（RGB + 1 + 1） 7 个通道（RGB + RGB + 1） 内存/显存开销 较低，Metallic 和 Roughness 常打包在同一张贴图的通道中 较高，Specular 需要额外的一张 RGB 贴图 能量守恒 强制守恒，通过 Shader 内部逻辑自动分配反射与漫反射比例，不易出错 依赖创作者，若 Diffuse 和 Specular 值设置过高，极易违背物理规律 非金属 $F_0$ 控制 缺乏灵活性，通常被引擎硬编码为 4% / 0.04 标量 极高灵活性，可为任何绝缘体自定义特殊的 $F_0$ 颜色和强度 边界过滤伪影 存在，金属与非金属交界处，贴图线性插值会导致白边/黑边伪影 无此问题，漫反射与镜面反射完全分离计算 4. 技术细节分析 边界伪影（Fringe Artifacts） 在 MR 模型中，当一个像素恰好处于金属与非金属的交界处时，GPU 纹理采样器的双线性插值（Bilinear Filtering）会对 Base Color 和 Metallic 贴图同时进行插值。这会导致在过渡区域出现半金属半绝缘体的错误物理状态，视觉上表现为明显的白边或黑边。\n在 SG 模型中，由于 Diffuse 和 Specular 是完全独立的 RGB 贴图，过渡区域的插值只在颜色空间内进行，不会引起底层物理状态的逻辑冲突，因此边界过渡更加自然。\n能量守恒约束 PBR 的核心原则之一是出射光能量不能超过入射光能量。\nMR 模型的安全性： 在着色器内部，漫反射能量计算通常被定义为 Diffuse = BaseColor * (1.0 - Metallic)。这种硬编码逻辑强制剥夺了金属的漫反射，确保了能量的物理正确性。 SG 模型的风险性： 由于允许手动指定 Diffuse 和 Specular，如果没有严格的校验，创作者可能会同时赋予同一区域高强度的漫反射和高强度的镜面反射，从而打破能量守恒。 工业界选择 MR 模型因其管线友好、显存占用低、容错率高，是现代引擎中常用的模型。SG 模型则在需要精确控制绝缘体折射率与高光表现（如复杂的宝石、布料或多层车漆）的特定材质管线中作为补充存在。\n","permalink":"https://jiat-t.github.io/graphics/pbr-models/","summary":"对比 PBR 中 Metallic-Roughness 与 Specular-Glossiness 两种材质工作流的贴图结构、能量守恒约束和显存开销。","title":"PBR 材质模型"},{"content":"官方文档中的流程图如下：\n因为步骤比较多，涉及到的源文件也非常繁杂，所以此处仅仅只是梳理一遍大概的渲染流程；之后我想我会更加具体地深入某一阶段的源码进行学习\u0026hellip;\u0026hellip;\n1. 场景准备与遮蔽 核心：决定“什么东西要画，什么东西不要画”\n在现代引擎的架构下，进行绘制操作时，性能的瓶颈往往不是着色器本身，而是 CPU 与 GPU 之间的高频通信，更具体地说是 CPU 向 GPU 提交的一个个 Draw Call（绘制指令）。我们都知道，CPU 想要将信息传递到 GPU，需要跨过一条漫长的 PCIE 总线，这一过程是极其耗费时间的（相对于 GPU 高速的绘制操作来说）\n因此，提前筛选出最终可能会用到的信息是非常关键的\n步骤 具体操作由 CPU 的两个线程(抽象意义上的线程）来执行 —— 游戏线程 与 绘制线程\n游戏线程 处理动画、物理模拟、AI\u0026hellip;\u0026hellip; 遍历场景树，收集所有挂载着渲染组件的物体（如 StaticMesh、Skeletal Mesh\u0026hellip;\u0026hellip;） 计算每个物体的世界矩阵，并将数据打包 之后 UE 会将需要渲染的物体同步给绘制线程\n绘制线程 这一步主要执行剔除工作\n距离剔除 根据事先设定好的“最大绘制距离”将超出这个阈值的物体直接从渲染列表中剔除\n应用：草丛、地上的碎石\u0026hellip;\u0026hellip;\n视锥剔除 场景中的每个可见物体都拥有包围盒 / 包围球，这一阶段会将包围盒与视锥体进行快速地相交测试，只要包围盒完全位于视锥体之外（比如玩家背面），就会被剔除\n遮挡剔除 相对于前两步的“暴力删除”操作来说，遮挡剔除会更加复杂与耗时；并且不同于传统渲染管线中的 Depth-Test，UE 使用的是基于 HZB（Hierarchical Z-Buffer，分层深度缓冲）的遮挡剔除技术\n在传统的 Z-Buffer 中，对于物体在屏幕上的每一个像素，都需要与 Z-Buffer 中对应的像素进行比较，虽然在工程实现上比较容易，但是一旦当物体占据了屏幕上的大量像素比如 500 x 500 个像素点时，那么光是测试这一个物体就需要读取与比较 250000 次深度值，开销极大\nHZB 则借鉴了 Mipmap 的思想，将原本巨大的深度图进行了降采样分层（每 2 x 2 个像素生成一个新像素，这个新像素的值则为其中最远的像素深度值）\n如图，从左至右 Mip 层级分别为 4、5、6\nhttps://miketuritzin.com/post/hierarchical-depth-buffers/\n这样一来，对于巨型物体（在屏幕上占据大量像素）使用低分辨率层级 HZB，较小的物体使用高分辨率层级 HZB，从而使得无论物体多大，引擎总能找到一个恰好匹配其大小的 HZB 层级，使得该物体的包围盒在那个层级下，只覆盖极少数的几个像素\n但是这样不会丢失精度吗？\n答案是基本不会损失精度。因为上一步仅仅只是决定用什么深度图，还没有进行比较。\n而在深度比较中，引擎会提取该物体距离摄影机最近的点与 HZB 中的深度值进行比较。而如果最近的点都比最远的遮挡物还要远，那么这个物体必不可能被摄影机看到，可以直接丢掉\n2. 几何体渲染 3. 光栅化与几何缓冲 4. 渲染纹理 5. 像素着色器与材质 6. 反射 7. 静态光照与阴影 8. 动态光照与阴影 9. 雾与透明度 10. 后处理效果 【Reference】https://dev.epicgames.com/documentation/unreal-engine/introduction-to-rendering-in-unreal-engine-for-unity-developers#rendering-in-unreal-engine\n原文：UE 渲染管线（桌面端延迟渲染）\n","permalink":"https://jiat-t.github.io/ue/ue-deferred-rendering-pipeline/","summary":"按场景准备、可见性筛选、几何缓冲、材质、光照与后处理梳理 UE 桌面端延迟渲染流程。","title":"UE 渲染管线（桌面端延迟渲染）"},{"content":"\n一：什么是序列化 最简单的说法是：将任意对象 / 数据编排为事先约定好的格式，使得之后的反序列化可以直接拿出来使用\n但是这里的“事先约定的格式”可能是非常复杂的，需要考虑多个方面，比如：如果对所有数据进行打包？序列化后的格式是否具有跨平台性？能否通过网络发送？\u0026hellip;\u0026hellip;\n二：UE 中的序列化 【Reference】\nhttps://zhuanlan.zhihu.com/p/2756361343\n原文：序列化\n","permalink":"https://jiat-t.github.io/ue/ue-serialization/","summary":"记录序列化的基本含义、数据格式需要考虑的问题及 UE 序列化参考资料。","title":"UE 序列化"},{"content":"先贴一个完整流程，后面再作具体讲解：\n通过 UFUNCTION()、UPROPERTY()、UCLASS() 等宏标记需要进入反射系统的数据。 UBT 在正式编译前调用 UHT，UHT 扫描头文件中被标记的类型、函数和属性。 UHT 生成对应的 .generated.h 和 .gen.cpp 等中间代码。 C++ 编译器继续编译项目代码和 UHT 生成的代码。 运行时可以通过 UClass、UFunction、FProperty 等元信息访问类型、函数和属性。 引擎基于这些元信息驱动编辑器显示、蓝图调用、序列化、网络复制和 GC 引用追踪等系统。 一：什么是反射，为什么需要反射？ 在 C++ 运行期，如果想要得到类、函数、变量的具体信息，通常是不可行的。C++ 编译完成后，变量名、函数签名、成员偏移等信息不会自动保留成一套可查询的运行时类型系统。\n反射的目的就是让引擎在运行期也能拿到这些结构化信息。以类为例，反射数据通常包含类名、父类、成员函数、成员变量、变量类型、成员在对象内存中的偏移值等。\nUE 不是反射所有 C++ 代码，只有被手动标记的类型、函数和属性才会被 UHT 捕获。UHT 会把这些信息生成到 .generated.h、.gen.cpp 等文件中，再参与后续编译。\n一句话总结：\nUE 反射系统把被标记的 C++ 类、属性、函数注册到引擎的类型系统里，让引擎能在运行时识别和操作它们。\n二：常用宏 UPROPERTY() 表示这个成员变量要被 UE 识别。它可以参与编辑器显示、蓝图访问、序列化、GC 引用追踪、网络复制等流程。\n1UPROPERTY(EditAnywhere, BlueprintReadWrite) 2int32 Health; UENUM() 用于枚举反射，方便蓝图和编辑器使用。\n1UENUM() 2enum class EWeaponType : uint8 3{ 4 Sword, 5 Bow 6}; USTRUCT() 用于结构体反射，比如背包物品数据、技能配置等。\n1USTRUCT() 2struct FItemData 3{ 4 GENERATED_BODY() 5}; UCLASS() 表示这个 C++ 类要进入 UE 反射系统。通常用于 UObject 派生类，比如 AActor、UActorComponent、UUserWidget 等。\n1UCLASS() 2class AMyActor : public AActor 3{ 4 GENERATED_BODY() 5}; UFUNCTION() 表示这个函数要被 UE 识别。它可以暴露给蓝图，也可以用于 RPC、事件、反射调用等。\n1UFUNCTION(BlueprintCallable) 2void Attack(); 三：UHT 与 UBT UHT 与 UBT 通常作为独立程序参与构建流程。简单说，UBT 负责组织 UE 项目的整体构建，UHT 负责从头文件里提取反射信息并生成代码。\nUHT（Unreal Header Tool） UHT 的核心职责是扫描头文件，找到 UE 特有的反射宏，例如 UCLASS()、USTRUCT()、UENUM()、UPROPERTY()、UFUNCTION()，然后生成对应的反射辅助代码。\n.generated.h 主要包含 GENERATED_BODY() 展开所需的声明、注册入口和样板代码。\n.gen.cpp 主要负责生成类、属性、函数等元信息的注册代码，让运行时能构造出引擎可识别的 UClass、FProperty、UFunction 等对象。\nUBT（Unreal Build Tool） 在传统 C++ 开发中，构建系统通常通过 CMake、Visual Studio 工程或其他工程文件来描述。UE 需要跨平台构建，同时代码规模很大，所以使用了自己的构建系统 UBT。\nUBT 的主要职责：\n解析依赖： UE 的模块通常带有 .Build.cs 和 .Target.cs 文件，用来描述模块依赖、目标类型和构建设置。UBT 会在编译前解析这些依赖关系。 调度构建： UBT 会检查文件改动，按需调用 UHT 生成反射代码，再调用 MSVC、Clang 等编译器完成实际编译，最后输出 .exe、.dll 等产物。 四：运行时的元信息层 反射信息虽然由 UHT 生成成 C++ 代码，但在运行时会表现为一组描述其他 C++ 类型的对象。也就是用 UClass、UFunction、FProperty 这类对象，去描述我们自己写的 AMyCharacter、Health、Attack() 等类、属性和函数。\n为什么需要单独的元信息层？\n因为普通 C++ 类和变量在编译后不会天然保留一套可供引擎查询的名字、类型、偏移、标记等信息。运行时如果只拿到对象地址，引擎不知道哪个成员叫 Health，也不知道它是不是蓝图可读写、是否需要序列化、是否是 UObject 引用。\nUHT 生成的反射注册代码会把这些信息整理出来。例如某个类的元信息里可以保存类名 \u0026quot;AMyCharacter\u0026quot;、类大小 sizeof(AMyCharacter)，以及属性数组。属性元信息中又可以保存 \u0026quot;Health\u0026quot;、属性类型、对象内偏移和各种标记。\nUClass 通过 UClass，UE 可以知道：\n类名 父类 类大小 构造方式 有哪些属性 有哪些函数 有哪些接口 默认对象 CDO 蓝图元数据 UFunction UFunction 表示一个函数的元信息，它能描述：\n函数名 参数列表 返回值 是否蓝图可调用 是否 RPC 是否事件 FProperty FProperty 表示一个属性的元信息，它能描述：\n属性名 属性类型 偏移量 是否可编辑 是否蓝图可读写 是否需要序列化 是否是 UObject 引用 五：反射与 GC 的关系 UE 的 GC 需要知道哪些 UObject 还可以被访问。对于被 UPROPERTY() 标记的 UObject 引用，反射系统会记录属性类型和内存偏移。GC 做可达性分析时，就能根据这些元信息找到对象内部保存的 UObject 指针，并继续沿着引用关系遍历。\n如果一个 UObject 指针只是普通裸指针，而且没有通过 UPROPERTY() 或其他 GC 引用收集机制暴露给引擎，GC 通常就无法把它当作有效引用追踪。这也是 UE 中 UObject 引用经常需要配合 UPROPERTY() 使用的原因。\nReference Unreal C++ 反射实现分析（一） Unreal C++ 反射实现分析（二） ","permalink":"https://jiat-t.github.io/ue/reflection-system/","summary":"整理 Unreal Engine 反射系统的作用、常用宏、UHT/UBT 生成流程，以及它和编辑器、蓝图、序列化、GC 的关系。","title":"UE 反射系统"},{"content":"Reference: Gameplay Framework in Unreal Engine\n一：规则与全局状态 这部分决定一局游戏如何开始、如何结束，以及哪些状态需要同步给所有玩家。\nGameMode 职责： 定义一局游戏的权威规则与流程，例如玩家如何加入、默认 Pawn / Controller / HUD / PlayerState 使用哪些类、如何生成玩家、胜负条件如何判定。\n网络特点： GameMode 只存在于服务器端。客户端不能直接访问或信任 GameMode，否则玩家就可以通过修改本地逻辑作弊。\n适合存放： 只需要服务器知道的规则判断，例如是否允许重生、是否满足胜利条件、玩家进入游戏时应该生成什么角色。\nGameState 职责： 保存“所有玩家都应该知道的全局状态”，例如当前比赛阶段、队伍分数、剩余时间、目标进度，以及所有玩家的 PlayerState 列表。\n网络特点： GameState 由服务器维护，并复制到所有客户端。它不是用来制定规则的，而是用来把服务器认可的比赛状态同步给所有人。\n容易混淆： “谁赢了、能不能重生、应该在哪里出生”这类规则通常由 GameMode 判定；“当前比分、比赛是否已经结束、所有玩家列表”这类共享结果适合放在 GameState。\nPlayerState 职责： 保存某个玩家的可复制状态，例如玩家名、分数、队伍、击杀数、死亡数、延迟，或者需要在换 Pawn 后仍然保留的玩家数据。\n生命周期： PlayerState 跟“玩家”绑定，而不是跟某个具体角色模型绑定。Pawn 死亡、重生或被替换时，PlayerState 通常仍然代表同一个玩家。\n网络特点： PlayerState 会复制给所有客户端，所以适合放其他玩家也需要看到的数据。只属于本地 UI 或本地输入的数据，不应该放在这里。\n二：控制与决策 这部分处理“谁在下命令”，以及命令如何作用到可控制实体上。\nController Controller 是非物理的“意志”或决策对象，负责控制一个 Pawn。Controller 本身不代表游戏世界中的身体，它通过 Possess 控制 Pawn。\nPlayerController 职责： 表示玩家的控制意图，处理输入、相机相关控制、与本地玩家 UI / HUD 的交互，并控制一个 Pawn。\n网络特点： 在多人游戏中，PlayerController 通常只存在于服务器和对应的拥有者客户端上，并不会像 PlayerState 那样完整复制给所有客户端。因此公共玩家信息更适合放在 PlayerState。\nAIController 职责： 表示 AI 的决策逻辑，可以运行行为树、黑板、状态树、导航等系统，并控制一个 AI Pawn。\n使用场景： NPC、怪物、自动炮台、机器人等不由人类玩家直接输入控制的实体。\n三：世界中的实体与组件 这部分解决“什么东西存在于世界中，以及它如何组成”。\nWorld World 是运行中关卡和 Actor 的上下文，包含当前加载的关卡、Actor、GameMode、GameState 等对象。它不是单个玩法规则类，而是游戏世界运行时的容器。\nActor Actor 是关卡中最基础的可放置、生成、复制和销毁对象。它本身更像一个容器，真正的可视化、碰撞、音效、粒子、移动能力通常由组件提供。\nComponent Component 用来把功能模块化地挂到 Actor 上。\nSceneComponent 提供 Transform 和层级关系。 PrimitiveComponent 可以参与渲染或碰撞。 MovementComponent 负责移动逻辑。 自定义 ActorComponent 适合封装背包、交互、状态机等可复用功能。 Pawn Pawn 是可以被 Controller 控制的 Actor，代表一个可被“意志”驱动的实体。玩家角色、AI 敌人、载具、无人机都可以是 Pawn。\nCharacter Character 是 Pawn 的常用子类，专门面向类人角色或需要角色移动模型的实体。\n默认常见组件包括：\nCapsuleComponent：胶囊碰撞体。 SkeletalMeshComponent：骨骼网格。 CharacterMovementComponent：角色移动组件，提供行走、跳跃、网络移动预测等常用能力。 如果实体不需要这套角色移动能力，例如飞船、炮塔、棋子，直接继承 Pawn 或 Actor 往往更合适。\n四：生命周期与跨关卡数据 这部分处理“哪些数据能跨关卡存在，以及模块化系统放在哪里”。\nGameInstance GameInstance 在游戏启动后创建，在游戏退出前一直存在，通常会跨关卡保留。它适合保存全局配置、账号状态、匹配入口状态、当前存档槽等不依赖单局比赛的运行数据。\n注意： GameInstance 不会像 GameState 一样自动复制给其他客户端。多人游戏里需要同步的数据应放在服务器权威对象中，再通过复制或 RPC 同步。\nGameInstanceSubsystem GameInstanceSubsystem 是跟随 GameInstance 生命周期的模块化功能对象。它适合拆分长期存在的服务，例如存档管理、音频设置、账号服务、全局资源管理等。\n如果功能只应该存在于某个世界或某个本地玩家，还可以根据需求使用 WorldSubsystem、LocalPlayerSubsystem 等更合适的 Subsystem 类型。\n五：视角、界面与辅助工具 Camera 相机表示玩家或系统观察世界的方式。它可以来自角色上的 CameraComponent，也可以来自独立的 CameraActor，并不只和玩家当前控制的角色有关。过场动画、观战视角、固定镜头、编辑器预览都可能使用不同的相机来源。\nHUD 与 UI HUD 是与玩家显示相关的 Actor，传统上用于绘制屏幕覆盖信息；现代项目中更常用 UMG / Widget 来构建菜单、血条、背包、准星等 UI。\n每个本地玩家通常拥有自己的界面层。需要所有玩家共享的数据不要直接存在 UI 里，而应该从 GameState、PlayerState 或其他权威数据源读取。\nGameplayStatics GameplayStatics 是一组静态辅助函数集合，用于快速访问常见玩法操作，例如获取玩家、打开关卡、播放音效、生成对象、查找 GameMode / GameState 等。\n它适合做便捷入口，但不适合把项目核心逻辑都塞进去。长期维护的系统逻辑更应该放在明确的 Actor、Component、Subsystem 或 Gameplay Ability 等结构中。\n总结 GameMode： 服务器权威规则 GameState： 所有人都能看到的全局状态 PlayerState： 某个玩家的可复制状态 Controller： 控制意图 Pawn / Character： 被控制的世界实体 Actor / Component： 世界对象与模块化能力 GameInstance / Subsystem： 跨关卡生命周期与长期服务 HUD / UI / Camera： 本地展示、交互和观察方式 ","permalink":"https://jiat-t.github.io/ue/gameplay-framework1/","summary":"按职责、网络复制和生命周期梳理 Unreal Engine Gameplay Framework 中常用类的分工。","title":"UE Gameplay Framework（一）"},{"content":"\n一：地形的几何 Heighfield： 根据高度图偏移顶点\n缺点：远处的细分层级太高，存在 LoD 的优化空间\n细分方式： 二分的方法：每次选取三角形最长边的中点进行切分 QuadTree-Based Subdivision：优点——容易构建、资源更易于管理\u0026hellip;，缺点——网格细分不如三角形灵活、叶节点的层级需要连续\u0026hellip;. 原文：大气与云的渲染\n","permalink":"https://jiat-t.github.io/graphics/games104-atmosphere-and-clouds/","summary":"记录高度场地形、三角形与四叉树细分的基本思路，以及地形 LOD 和层级管理问题。","title":"GAMES104：大气与云的渲染"},{"content":"在 Vulkan 中，由于驱动层不会隐式地进行任何自动同步，因此所有的资源读写、执行顺序等操作都需要手动地进行管理。 Vulkan 核心的同步原语有四种：Fence（栅栏）、Semaphore（信号量）、Event（事件） 和 Pipeline Barrier（管线屏障）。\n1. Fence（栅栏）：CPU 与 GPU 之间的同步 Fence 主要用于让 CPU 等待 GPU 完成某些操作。\n特点： 它有两种状态：Signaled（触发/绿灯）和 Unsignaled（未触发/红灯）。通常由 GPU 在执行完某个 Queue 的命令后将其置为 Signaled，而 CPU 在另一端阻塞等待。 常见用途： 帧率控制（避免 CPU 提交命令的速度远超 GPU 渲染速度）。例如，在开始录制新一帧的 Command Buffer 之前，CPU 必须等待上一帧对应的 Fence 被触发。 核心 API： * vkQueueSubmit（传入 Fence，当 Queue 中的任务执行完毕时，GPU 会自动将该 Fence 设为 Signaled） vkWaitForFences（CPU 端阻塞等待）vkResetFences（使用前必须手动重置为 Unsignaled） 2. Semaphore（信号量）：GPU Queue 之间的同步 Semaphore 用于 GPU 内部不同 Queue 之间 或者 同一 Queue 内不同提交（Submit）之间 的粗粒度同步。CPU 无法直接等待或触发它。\nVulkan 中的 Semaphore 分为两种：\nBinary Semaphore（二元信号量）： 只有两种状态（Signaled/Unsignaled）。常用于交换链图像获取与渲染的同步。 Timeline Semaphore（时间轴信号量，Vulkan 1.2+）： 包含一个单调递增的 64 位整数值。当它的值大于或等于指定的等待值时，同步完成。它允许更灵活的粗粒度依赖管理（甚至支持 CPU 等待或增加其数值）。 常见用途：vkAcquireNextImageKHR（获取交换链图像后触发 Semaphore） $\\rightarrow$vkQueueSubmit（等待该 Semaphore 产生后才开始渲染渲染管线）。 核心机制： 在 vkQueueSubmit 的 VkSubmitInfo 结构体中，分别填入 pWaitSemaphores（执行前需要等待谁）和 pSignalSemaphores（执行完后去触发谁）。 3. Pipeline Barrier（管线屏障）：GPU 内部 / 管线阶段之间的同步 Pipeline Barrier 是 Vulkan 中最频繁、最精细、也最复杂的同步手段。它用于 同一 Queue 内部的不同命令之间 的同步，解决管线内各阶段（Pipeline Stages）的读写冲突，并负责处理内存可见性（Memory Visibility）和图像布局转换（Layout Transition）。\n当你在 Command Buffer 中插入一个 Barrier 时，需要显式指定以下几个要素：\nSource Stage / Destination Stage（源阶段 / 目标阶段）： 规定了哪些先前的指令（Src）必须在哪些后续指令（Dst）开始之前执行完毕。例如：等待顶点着色器（VS）写完，才允许片段着色器（FS）去读。 Access Masks（访问掩码）： 规定了内存的读写类型，解决 RAW（写后读）、WAR（读后写）、WAW（写后写）冲突。Execution Dependency（执行依赖）： 确保 Dst 阶段在 Src 阶段结束后才执行。Memory Dependency（内存依赖）： 确保 Src 阶段写入 L1/L2 Cache 的数据刷新到全局内存（Make Available），并让 Dst 阶段能正确从内存读取（Make Visible）。 Image Layout Transitions（图像布局转换）： GPU 对不同用途的 Image 有不同的内部瓦片（Tiling）或压缩优化。在将一个 Image 从“渲染目标（Color Attachment）”变成“采样纹理（Shader Read Only）”时，必须通过 Image Memory Barrier 进行布局转换，否则会导致硬件读取错误。 4. Event（事件）：GPU 内部的细粒度延迟同步 Event 类似于 Pipeline Barrier 的“拆分版”，它允许异步的、非阻塞的在 GPU 管线中标记进度。\n特点： Barrier 是立刻在命令流中划定一道分界线，而 Event 允许你在前面的命令走到某个阶段时“设置（Set）”它，在很久之后的另一个地方再去“等待（Wait）”它。 常见用途： 减少 GPU 的管线气泡（Bubble）。例如，你在管线早期生成了一段数据，只要在该数据真正被使用的后继阶段之前进行等待即可，中途的其他管线阶段可以并发执行。 核心 API：vkCmdSetEvent 和 vkCmdWaitEvents。 总结与对比 为了理清它们的关系，可以通过它们在流水线中的协作层次来区分：\n同步原语 控制主体 作用域 粒度 主要应用场景 Fence CPU 与 GPU 整个 Queue 提交的任务 最粗 帧同步、等待一帧彻底渲染完、重建交换链 Semaphore GPU 与 GPU Queue 之间 或 Submit 之间 粗 等待 Image 可用、计算队列与渲染队列交替 Pipeline Barrier GPU 内部 同一 Queue 的 Command 之间 细（管线阶段级） 资源读写保护、Texture Layout 转换 Event GPU 内部 同一 Queue 的 Command 之间 精细（支持异步拆分） 消除复杂的依赖气泡、细粒度资源复用 原文：同步原语\n","permalink":"https://jiat-t.github.io/vulkan/vulkan-synchronization-primitives/","summary":"比较 Fence、Semaphore、Pipeline Barrier 与 Event 的同步范围、常见用途和相关 API。","title":"Vulkan 同步原语"},{"content":"在 Vulkan 中，Layers（层） 和 Extensions（扩展） 是实现其“极致性能”与“跨平台可扩展性”的两大核心机制。\n简单来说：\nLayers（层） 专注于 “检查与调试”（它是可插拔的中间件，负责在代码和驱动之间做拦截）。\nExtensions（扩展） 专注于 “功能特征的扩充”（它负责解锁核心 Vulkan 标准之外的新硬件特性或平台特定功能）。\n1. Layers（层 / 验证层） Vulkan 为了追求绝对的高性能，它的驱动程序（Driver）非常“盲目”：它默认程序员编写的代码是 100% 完美的。因此它不会在运行时检查空指针、不会检查内存越界，也不会验证状态绑定是否正确。如果传错了参数，Vulkan 驱动会直接装作不知道，然后导致程序直接崩溃（Crash）或者引发 GPU 掉线（Device Lost）。\n为了不把这些繁重的“检查代码”塞进正式发布的驱动里，Vulkan 引入了 Layers 机制。\n核心特点： 可插拔的中间件： 它们像一个个滤网一样，挂在 Vulkan 应用和底层驱动之间。当调用 Vulkan 函数时，会先穿过这些 Layers，最后才到达真正的显卡驱动。\n开发时开启，发布时关闭： 在开发阶段，可以开启它们来捕捉错误；当游戏或程序准备发布（Release）时，代码会直接不加载它们，这样应用就能以零开销的纯净状态在电脑上飞速运行。\n最著名的层：验证层 (VK_LAYER_KHRONOS_validation) 这是写 Vulkan 代码必开的层。它由 Khronos 官方维护，集成了以下功能：\n检查代码是否违反了 Vulkan 规范（比如参数传错、乱填枚举）。\n监控内存泄漏（比如是否用完 VkDeviceMemory 忘记 free）。\n检测多线程资源竞争（Hazard）。\n如果不开启它，写 Vulkan 就像是在没有编译器的纯文本编辑器里盲写代码。\n2. Extensions（扩展） Vulkan 是一个跨平台的标准（支持 Windows, Linux, Android 等），并且需要兼容各种各样的硬件（NVIDIA, AMD, Intel, ARM 等）。\n但是，不同平台有不同的窗口系统，不同显卡也有自己独有的独家绝活（比如 NVIDIA 的某些特定硬件光追特性，或者最新的 NPU 联合渲染功能）。Vulkan 核心规范（Core Spec）不能把这些零碎的东西都塞进去。于是，Extensions 应运而生。\n核心特点： 功能开关： 扩展用来暴露那些核心 Vulkan 规范里没有定义的功能。\n分级命名： 扩展的命名直接体现了它的通用程度：\nKHR (Khronos 扩展)： 官方批准的通用扩展。例如 VK_KHR_swapchain（交换链扩展，用来把画面显示到屏幕上）。\nEXT (多厂商扩展)： 几个硬件厂商一起支持的扩展。\nNV / AMD / INTEL (特定厂商扩展)： 只有特定显卡才支持的独占大招。例如 VK_NV_ray_tracing（早期的 NVIDIA 硬件光追扩展）。\n3. Layers 与 Extensions 的协同分类 在 Vulkan 中，无论是 Layers 还是 Extensions，都严格分为两个层级（虽然在现代 Vulkan 1.1+ 中，Instance 层的部分概念得到了简化，但逻辑依然清晰）：\n级别 作用范围 常见示例 Instance 级别 (实例) 影响整个 Vulkan 全局，与具体显卡无关。通常与上下文初始化或全局平台扩展相关。 VK_LAYER_KHRONOS_validation (全局验证)VK_KHR_surface (全局窗口表面扩展) Device 级别 (逻辑设备) 只影响你选中的那一张特定显卡。通常与硬件底层功能相关。 VK_KHR_swapchain (允许该显卡将图像呈现给窗口)VK_KHR_acceleration_structure (开启该显卡的光追加速结构支持) 总结 Layers 像“监控摄像头”： 挂在 API 之上，盯着代码有没有违规操作，随时报 Bug。发布时直接全部拆掉。\nExtensions 像“DLC 扩展包”： 用来给 Vulkan 打补丁。想要支持窗口显示？装一个 Swapchain 扩展。想要支持最新的硬件光追（Ray Tracing）或网格着色器（Mesh Shader）？装一个对应的厂商硬件扩展。\n","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/layers-and-extensions/","summary":"梳理 Vulkan Layers 与 Extensions 的职责、层级和常见用途。","title":"Vulkan Layers 与 Extensions"},{"content":"如果把 Vulkan 的底层驱动比作一辆没有限速器、没有安全气囊的专业 F1 赛车，那么验证层就是在跑赛道（开发阶段）时坐在副驾驶上的功勋教练。他会死死盯着每一次换挡和转向，一旦发现危险，立刻对驾驶员大喊。而当赛车正式比赛（发布 Release）时，这位教练会直接下车，让赛车以零干扰的极致速度裸奔。\n1. 为什么 Vulkan 必须要引入验证层？ 在传统的图形 API（如 OpenGL 或 DirectX 11）中，驱动程序内部塞满了大量的安全检查代码。比如调用一个画三角形的函数，OpenGL 驱动会在后台默默检查：\n传进来的顶点缓冲区指针是不是空的？\n绑定的纹理格式对不对？\n当前的状态机有没有冲突？\n这些检查非常温馨，但代价极其高昂：它们是在运行时、在每一帧里、由 CPU 隐式执行的。这意味着即使代码已经完美无瑕，这些检查依然在疯狂压榨 CPU 性能，造成了极大的运行时开销\nVulkan 为了追求极致的性能，做了一个极其大胆的决定：核心驱动默认代码是 100% 完美的。\n驱动内部解除了几乎所有的参数验证。\n如果传错了参数（比如绑定了不兼容的内存），驱动不会报错，也不会抛出异常，它会直接装作不知道。\n最终的结果通常是：画面莫名其妙花屏、游戏直接闪退（Crash），或者最致命的——触发 VK_ERROR_DEVICE_LOST（整个显卡硬件挂起、掉线，甚至导致驱动蓝屏重置）。\n为了既能保留极致性能，又能让程序员活下去，Vulkan 提出了 可插拔的验证层机制。\n2. 验证层的工作原理与层叠架构 验证层的本质是一个或者多个可插拔的动态链接库（在 Windows 上是 .dll，在 Linux 上是 .so）。\n当代码没有开启验证层时，API 调用是直达显卡驱动的。而当开启验证层后，Vulkan 会通过一种“钩子（Hooks）/ 拦截”机制，把这些动态库像滤网一样层层叠在应用程序和驱动之间：\n当调用类似 vkCreateImage 这样的函数时：\n请求首先进入验证层。\n验证层会在 CPU 端根据 Vulkan 官方规范（Spec），对传入的结构体参数进行严格的静态和动态审查。\n如果发现违规，验证层会立刻通过DebugUtilsMessenger 回调函数，在控制台打印出极其详细的错误日志。\n审查通过后，验证层才把这个调用转发给真正的显卡驱动。\n3. 验证层能监控什么？ 现代 Vulkan 开发中，官方将所有的验证功能打包到了一个核心层中，叫做 VK_LAYER_KHRONOS_validation。它内部集成了多个强大的监控模块：\n① 内存管理和泄露监控 (Mem Tracker) 在 Vulkan 中，程序员需要显式地用 vkAllocateMemory 申请显存，并用 vkFreeMemory 释放。\n验证层会监控每一笔显存开销。如果在销毁逻辑设备（VkDevice）时，还有缓冲区（Buffer）或图像（Image）没释放，它会直接列出该资源的内存句柄，报出内存泄露（Memory Leak）。\n它还会检查代码是否往一个尚未绑定（Bind）任何物理显存的句柄里写入了数据。\n② 状态有效性验证 (Object Tracker) 检查使用的 Vulkan 对象生命周期是否合法。\n比如当我们尝试销毁一个正在被 GPU 队列执行的命令缓冲区（Command Buffer），或者销毁一个正在被渲染管线使用的采样器（Sampler），验证层会立刻发出严厉警告。 ③ 线程安全与资源竞争检测 (Threading) Vulkan 支持极其强悍的多线程指令录制，但这也带来了并发中存在的一些问题。\n规范规定：多线程不能同时往同一个 VkQueue（队列）或同一个 VkCommandPool（命令池）中提交或录制命令。\n验证层会在后台监控 CPU 线程间的行为。一旦发现两个 C++ 线程在没有加锁或没有信号量同步的情况下争抢同一个 Vulkan 句柄，就会立刻触发冲突报警。\n④ 隐式性能警告 (Best Practices / Performance) 这是高端游戏引擎开发极度依赖的功能。有时候代码完全合法，画面也出得来，但验证层会弹出一个性能警告（Performance Warning）：\n“警告：你正在频繁地通过 CPU 映射（vkMapMemory）去修改一块经常变动的顶点数据，这会导致严重的 PCIe 总线带宽瓶颈，建议使用 Staging Buffer（暂存缓冲区）进行异步传输。” 4. 验证层在工程开发中的落地生命周期 正如之前讨论的，验证层遵循严格的 RAII 思想和开发/发布分离原则。\n阶段一：开发环境（Debug 模式） 在工程里，通常会通过宏定义或者编译器开关来控制验证层的开启：\n1 2#ifdef NDEBUG 3 4 const bool enableValidationLayers = false; 5 6#else 7 8 const bool enableValidationLayers = true; // Debug 模式下强行开启 9 10#endif 此时，验证层全力工作。虽然它会由于大量的 CPU 侧检查导致帧率从 300 帧直接跌到 60 帧，但它能确保写出来的代码绝对符合 Vulkan 官方规范。\n阶段二：工业发布（Release 模式） 当要把游戏打包发给玩家时，直接将 enableValidationLayers 设为 false。\n此时，Vulkan 实例在初始化时完全不加载 VK_LAYER_KHRONOS_validation 动态库。中间的滤网被彻底抽干，代码以纯净、裸奔的姿态直通显卡硬件。由于之前在开发阶段已经被验证层“拷打”得极为健壮，此时的代码在用户的各种显卡上运行都会表现得既稳又快。\n5. 验证层报错了，该怎么看？ 当开启验证层并绑定了 DebugUtilsMessenger 后，如果写错了代码，控制台通常会弹出类似下面这样一大串密密麻麻的英文：\n1[ Vulkan Debug - ERROR ] VALIDATION_LAYER: Validation Error: [ VUID-vkCmdDraw-commandBuffer-02701 ] Object 0: handle = 0x55aa66bfd0, type = VK_OBJECT_TYPE_COMMAND_BUFFER; | Message: vkCmdDraw(): Cannot call Draw inside a RenderPass that has not been started yet via vkCmdBeginRenderPass(). 拆解这份“犯罪现场报告”： ERROR：定性。这是致命错误，必须修复。\nVUID-vkCmdDraw-commandBuffer-02701：这是该错误在 Khronos Vulkan 官方标准白皮书里的唯一条款编号（Valid Usage ID）。如果你对报错信息有疑惑，直接去 Google 搜索这个 VUID，能看到最权威的规范解释。\nObject 0: handle = ...：说明是哪一个具体的 Vulkan 句柄对象（这里是命令缓冲区）出事了。\nMessage：核心原因。这里明确告诉我们：“你试图调用 vkCmdDraw 画图，但是你竟然连 vkCmdBeginRenderPass（开启渲染通道）都还没调用，这是严厉禁止的！”\n","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/validationlayers/","summary":"整理 Vulkan Validation Layers 的作用、工作原理、监控范围和调试信息阅读方式。","title":"Vulkan 验证层"},{"content":"在 Vulkan 和图形学中，交换链（Swapchain） 是连接“游戏渲染引擎”和“显示器屏幕”之间的核心桥梁。\n简单来说，它的本质是一个由多个图形缓冲区（Images/Buffers）组成的队列（通常是 2 个或 3 个），负责在后台悄悄接收显卡画好的图像，并在恰当的时机把图像“交换”到屏幕上显示出来。\n1. 为什么需要交换链？（解决“撕裂”问题） 为了理解交换链，我们先看看如果没有它会发生什么。\n假设显卡直接往屏幕要显示的内存（帧缓冲区）里画画：显卡画得极快，而屏幕刷新得很慢（比如 60Hz 屏幕，每 16.6 毫秒才从上到下刷新一次）。\n当显卡画到一半，屏幕刚好开始刷新，屏幕就会把上一帧的下半部分和当前帧的上半部分一起显示出来，这就造成了画面断层的屏幕撕裂（Screen Tearing）现象。\n2. 交换链的工作原理：前后缓冲区 交换链通过引入多个缓冲区和轮流交替机制完美解决了这个问题。最经典的是双缓冲区（Double Buffering）机制：\n后缓冲区（Back Buffer）：显卡（Vulkan）正在默默在后台拼命绘制画面的那张画布。\n前缓冲区（Front Buffer）：当前正在被显示器读取并显示在屏幕上的那张画布。\n经典的工作循环如下：\n渲染：显卡把游戏画面渲染到后缓冲区。\n提交（Present）：显卡画完了，向交换链发出申请：“我画好了，准备展示！”\n交换（Swap / Flip）：在屏幕刷新的间隙（垂直同步 VBlank 时期），交换链将前、后缓冲区对调。原来的后缓冲区变成前缓冲区显示在屏幕上；原来的前缓冲区变成新的后缓冲区，等待接收下一帧。\n3. 三缓冲区（Triple Buffering）与展示模式 随着硬件发展，双缓冲区也存在瓶颈：如果显卡画得太快，画完后缓冲区后，必须干等着屏幕刷新才能交换，显卡就被迫“卡住”停工了。\n于是交换链引入了三缓冲区（1个前缓冲区，2个后缓冲区），并提供了不同的展示模式（Present Modes）：\nVK_PRESENT_MODE_FIFO_KHR（强启垂直同步）： 缓冲队列像一个先进先出的管道。如果队列满了，显卡必须排队等待屏幕刷新。优点：绝对不撕裂；缺点：有输入延迟。\nVK_PRESENT_MODE_MAILBOX_KHR（三倍缓冲/邮箱模式）： 如果显卡画得比屏幕刷新快，它不会停工，而是直接用最新画好的帧覆盖替换掉掉队列里还没来得及显示的旧帧。优点：绝对不撕裂，且延迟极低，是 Vulkan 最理想的模式。\nVK_PRESENT_MODE_IMMEDIATE_KHR（关闭垂直同步）： 显卡一画完，不管屏幕刷到哪里，立刻硬塞给屏幕。优点：延迟最低；缺点：画面疯狂撕裂。\n4. 为什么 Vulkan 的交换链很特殊？ 在老旧的 OpenGL 中，交换链（Window System Integration）是被强行封装在 API 内部的，开发者很难插手控制。\n而在 Vulkan 中，交换链是一个明确的、需要开发者手动创建和管理的显卡对象（VkSwapchainKHR）。\n需要亲自查询显卡支持多少种分辨率、支持什么色彩格式（如 RGBA 还是 BGRA）。\n需要自己决定是要双缓冲还是三缓冲。\n需要亲自去交换链里“索要”一张当前空闲的画布（vkAcquireNextImageKHR），画完之后再亲自把它“推”给屏幕（vkQueuePresentKHR）。\nVulkan 交换链创建的流程 第一步：确认硬件是否支持“显示” Vulkan 是一个跨平台的 API，有些场景（比如服务器端跑离线渲染、高性能计算）是不需要显示画面的。\n确认队列能力：首先要向物理显卡确认，当前挑选的显卡硬件队列，是否有能力把画面“推”到当前创建的窗口桌面上。\n开启交换链扩展：在正式激活逻辑设备（Device）时，明确告诉显卡需要开启呈现扩展服务。\n第二步：向窗口系统“查家底”（查询支持参数） 每个人的显示器、显卡和操作系统组合起来，能支持的画面特性都是不一样的。因此需要从硬件那里查到以下三种关键信息：\n基础表面能力：当前窗口的宽高是多少像素？最少需要几个缓冲区（双缓冲还是三缓冲）？最大支持多少个？画面支不支持旋转、翻转？\n像素格式列表：支持哪些色彩格式？例如是红绿蓝（RGB）还是蓝绿红（BGR）？支持不引人注目的普通色彩，还是支持 sRGB 伽马校正的色彩空间？\n垂直同步模式：支持哪些冲刷画面的方式？是支持立即显示（会导致画面撕裂），还是支持严格的垂直同步（无撕裂但锁帧），或者是支持三重缓冲（既不撕裂又低延迟）？\n第三步：制定策略，挑选最优配置 拿到上一步的支持列表后，程序开始做选择题，挑出最合适的参数：\n定数量：通常在硬件允许的最小缓冲区数量上 + 1，尽量凑出三缓冲，让渲染和显示各不干扰。\n定分辨率：让交换链的画面宽高完美匹配当前窗口的物理像素大小。\n定格式：优先挑选符合人眼视觉习惯的 sRGB 渲染格式。\n定模式：如果硬件支持，优先选择三重缓冲（Mailbox），否则用强制保底的传统垂直同步（FIFO）。\n第四步：结合业务需求，正式向显卡申请创建 把上面挑选好的参数全部打包。在这一步，还需要把核心用途位掩码也加进去：\n明确告诉显卡，这批图像主要用来当作颜色输出画布。\n顺便探测一下，如果显卡支持，把截图、数据复制、后处理等权限也一并申请过来。\n处理队伍矛盾：告诉系统，如果图形渲染队列和画面呈现队列不是同一个硬件通道，这批图像需要开启“共享模式”以便它们互相传递。\n最终，向 Vulkan 驱动发出指令，正式在显存里开辟出这一组交换链对象。\n第五步：认领并接管这一组“隐形画布” 交换链成功创建后，Vulkan 在底层其实已经帮你创建好了一组图像（Image）。\n此时，程序需要向交换链“索要”这几张画布的控制句柄，并把它们存进一个数组里。\n只有拿到这些句柄，渲染管线才知道以后要把画面画到哪里去。\n第六步：给每张画布装上“镜头”（创建图像视图） 在 Vulkan 中有一个非常严格的规定：不能直接去读写一张原始的图像，而必须通过一个媒介去“看待”它。 这个媒介就是图像视图（Image View）。\n需要为数组里的每一张交换链画布，单独包装一个对应的图像视图。\n图像视图就像是一个说明书，它明确告诉显卡：虽然这是一块原始内存，但请把它当成一个标准的、2D 的、只关注红绿蓝蓝四个通道的颜色渲染目标来看待。\n最终状态 当第六步的图像视图全部包装完毕后，整个交换链的初始化才真正宣告结束。\n接下来，这些图像视图就会被送进帧缓冲区（Framebuffer）里，和渲染流程（Render Pass）绑定。显卡就可以开始开足马力，在这些画布上轮流画画，并把画好的成品源源不断地推送到显示器上了\n为什么创建交换链之后需要调用回调函数 核心原因是为了通知并同步整个渲染引擎的其他模块，让他们根据新的窗口状态重新调整自己的“画布”和依赖资源。\n具体来说，主要有以下四个深层次的原因：\n1. 视口（Viewport）与裁剪区（Scissor）需要同步更新 交换链一旦创建或重建，意味着窗口的大小（分辨率）可能已经发生了变化。\n图形管线在绘制时，需要明确知道要把画面投射到多大的屏幕区域（Viewport），以及哪些区域需要被裁剪（Scissor）。 触发回调函数可以第一时间把最新的窗口宽高（Width / Height）传给渲染器，动态更新视口和裁剪区的大小，防止画面出现拉伸、变形或者只渲染在屏幕角落的情况。 2. 依赖屏幕分辨率的资源（如深度缓冲区）需要随之重建 在渲染 3D 游戏或复杂场景时，除了交换链提供的颜色画布（Color Buffer）外，我们通常还需要其他辅助画布，比如深度缓冲区（Depth Buffer）或抗锯齿缓冲区（MSAA Buffer）。\n这些缓冲区的大小必须与交换链的图像大小完全一致。 交换链本身不负责创建深度图。因此，当交换链创建完毕后，必须通过回调函数通知负责管理资源的模块：“新画布的尺寸变了，请立刻销毁旧的深度图，并按照新尺寸重新申请一块深度显存。” 3. 重新构建帧缓冲区（Framebuffer） 在 Vulkan 中，交换链的图像视图（Image View）和深度视图（Depth View）必须打包绑定在一起，塞进一个叫做帧缓冲区（Framebuffer）的对象里，渲染管线（Render Pass）才能正常工作。\n既然交换链变了（旧的图像视图已被销毁，新的已创建），那么原本的 Framebuffer 自然就失效了。 回调函数的一个核心任务，就是拿到全新的一组交换链图像视图，结合刚刚重建的深度视图，重新生成一整套 Framebuffer 数组，为下一帧的录制做好准备。 4. 录制命令缓冲区（Command Buffer）的解冻与重录 如果你的引擎设计采用了静态/预录制命令缓冲区的策略（即把渲染命令提前录制好，每帧直接提交以换取极致性能）：\n命令缓冲区内部记录了具体的 Framebuffer 绑定信息和 Viewport 状态。 一旦交换链重建，这些录制好的命令就全部报废了。通过回调函数，引擎可以安全地重置（Reset）这些命令缓冲区，并根据新的交换链状态重新录制一遍绘制指令。 总结 交换链就像是显卡和显示器之间的一个高级传送带。显卡在传送带的一头不停地把画好的图片放上去，显示器在传送带的另一头按部就班地拿去播放。它不仅保障了画面的平滑顺畅，还掌控着游戏帧率和画面延迟的命脉。\n","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/swapchain/","summary":"整理 Vulkan Swapchain 的作用、展示模式、创建流程以及重建后的资源同步。","title":"Vulkan 交换链"},{"content":"在 Vulkan 的设计哲学中，为了达到对硬件的绝对掌控，它把“显卡”这个概念极其显式地拆分成了两个层面：物理设备（Physical Device） 和 逻辑设备（Logical Device）。\n简单来说：\n物理设备 代表的是你电脑里那块看得见、摸得着的整块 GPU 显卡芯片（硬件实体）。\n逻辑设备 代表的是你的应用程序与这块显卡之间建立的软件通信通道/会话（软件接口）。\n1. 物理设备 (VkPhysicalDevice) 物理设备 是对系统当前实际插在主板上的硬件 GPU 的抽象。\n核心特点 只读的硬件底牌： 你不能“创建”一个物理设备，你只能去“枚举（Enumerate）”和“查询（Query）”它。它就像是显卡厂商直接写死在硬件和驱动里的“说明书”。\n区分不同的显卡： 如果你的电脑（比如游戏笔记本）同时有 Intel 核显和 NVIDIA 独显，Vulkan 就会枚举出两个不同的 VkPhysicalDevice 句柄。\n能够查询到什么？ 通过 Vulkan 提供的查询函数（如 vkGetPhysicalDeviceProperties 和 vkGetPhysicalDeviceFeatures），你可以把这块硬件的底细摸得一清二楚：\n基础属性 (Properties)： 显卡名称（例如 RTX 5070 Ti）、厂商 ID、驱动版本、以及它是独立显卡（Discrete）还是集成显卡（Integrated）。\n硬件特性 (Features)： 这块显卡支不支持硬件光线追踪？支不支持网格着色器（Mesh Shader）？支不支持几何着色器？\n内存信息 (Memory Properties)： 这张卡有多少显存（Device Local Memory）？有多少可以直接映射的系统内存？\n队列族 (Queue Families)： 之前聊过的，它有哪些车间？支持图形、计算还是传输？\n2. 逻辑设备 (VkDevice) 逻辑设备 是你在选中某块物理设备后，向它正式申请并创建出来的虚拟操作接口。\n核心特点 真正干活的句柄： 绝大多数 Vulkan 的核心操作（创建缓冲区、创建纹理、创建渲染管线、创建着色器模块等），其函数的第一个参数全都是 VkDevice。\n按需定制： 创建逻辑设备时，你是在对物理设备说：“我决定用你了，但我不需要你的全部功能，我只要开启你的光追特性，并且帮我从图形队列族里实例化出一个队列来。”\n多实例并行： 针对同一个物理设备，你可以创建多个独立的逻辑设备。它们在同一个程序内互不干扰。\n3. 两者的关系与生命周期 物理设备和逻辑设备的关系，非常符合现代 C++ 的资源管理逻辑：\n多对一的选择： 系统中可以有多个物理设备，但你的程序通过筛选算法，最终通常只会选择其中一个最合适的物理设备。\n一对多的实例化： 选定物理设备后，基于它创建出一个或多个逻辑设备。\n生命周期分离：\nVkPhysicalDevice 的生命周期跟随 Vulkan 实例（VkInstance），不需要也不能手动销毁。\nVkDevice 必须遵循 RAII 原则。在程序退出或切换显卡时，你必须显式调用 vkDestroyDevice(device, nullptr) 来释放它以及挂在它底下的所有资源。\n总结 物理设备（Physical） 回答的是 “硬件能做到什么”（Capabilities）。\n逻辑设备（Logical） 回答的是 “我的程序要用它做什么”（Activation）。\n这种分离保证了 Vulkan 的纯净性：无论用户的电脑插了多少张卡，通过物理设备盘点清楚后，你的图形引擎主体只需要抱着创建好的逻辑设备（VkDevice）一路无脑画画就可以了。\n","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/physical-and-logic-device/","summary":"梳理 Vulkan 中 VkPhysicalDevice 与 VkDevice 的职责、关系和生命周期。","title":"Vulkan 物理设备与逻辑设备"},{"content":"\n一：游戏渲染中的挑战 游戏画面中可能有成千上万种效果需要绘制，非常复杂 引擎必须要深度适配现代的硬件 需要保持每一秒帧数的稳定 对 CPU 的占用不能过高，需要保留算力供其他模块调用 二：了解 GPU 1. SIMD（Single Instruction Multiple Data) 与 SIMT（Single Instruction Multiple Threads) SIMD（单指令多数据流）：将多个相同通道的数据打包进入一个超大的寄存器，仅调用一条指令，让一个算术逻辑单元 （ALU）对这些数据执行相同操作SIMT（单指令多线程）：将大量独立线程组织成逻辑组，并行的对数据进行处理 2. GPU 架构 GPC（Graphics Processing Cluster）SM（Streaming Multiprocessor）Texture UnitsCUDA CoreWarp 3. CPU 与 GPU 之间的数据流动 在游戏引擎中有一个原则：尽可能只让数据单向传输，即只让数据从 CPU 传入 GPU，而不从 GPU 中读取数据并传输到 CPU\n同时要利用好 Cache 的作用——因为如果 CPU 无法在自己的 Cache 中寻找到所需数据的话，就需要去内存寻找，而这会极大拖慢运行速度\nCache Hit：CPU 在缓存上找到数据 Cache Miss：CPU 无法在缓存上找到数据\n三：可渲染物体 组成：Mesh Primitive、Vertex Buffer 与 Index Buffer、Material、Textures、Shaders\n对于一个 mesh，根据材质的不同，会被切分为多个 submesh；一个 buffer 用来存储一整个 mesh，而每一个 submesh 占其中一小段\n但是如果对于每一个几何体，都开辟一个 buffer 去存储数据，会造成内存浪费（比如：多个物体共用一张贴图，但是却开辟了多个内存空间为每个物体单独存储一张相同的贴图）\n解决方法——Resource Pool：\n将 Shader、Texture、Mesh 等数据放入一个 Pool 中，确保资源可以复用\n四：可见性裁剪 通过判断包围体是否位于视锥体中来执行裁剪操作\n常见的包围体：Sphere、AABB、OBB\u0026hellip;\u0026hellip;\n层次视锥剔除：Quad Tree Culling、BVH\u0026hellip;\u0026hellip;\n现代游戏中通常存在着大量可动的物体，因此包围结构不仅要能够进行快速地求交，更要能够执行快速地重建，而 BVH 恰好满足这几点，这也是现代游戏引擎中用的最多的算法\nPortal and PVS Data： Portal（门）与 PVS（预计算可见性集合）都是经典的室内空间剔除技术，Portal 是在运行时由 CPU 沿着房间之间相连的“门窗”进行递归裁剪，只渲染视野穿过这些孔洞所能看到的区域；而 PVS 则是将世界切成网格，在离线阶段提前烘焙出每个网格点“可能看到”的物体列表，运行时玩家走到哪就直接 $O(1)$ 查表剔除，不耗费运行时算力。两者皆以结构的封闭性换取极致的渲染性能\nGPU Culling： 指的是将原本由 CPU 负责的画面可见性裁剪工作，完全移交给 GPU（通常利用 Compute Shader）来完成的技术。\n在传统渲染管线中，CPU 需要逐个计算物体的包围盒是否在视野内，然后再向 GPU 发送渲染指令（Draw Call）。当场景中物体达到数十万级别时，CPU 会因庞大的计算量和频繁的提交产生严重的性能瓶颈。\n而 GPU Culling 颠覆了这一流程：CPU 直接把场景中所有物体的数据一次性打包扔给显存，随后 GPU 利用自身恐怖的并行算力，在微秒级别内瞬间完成数十万物体的视锥体和遮挡测试，直接在显存中生成最终需要渲染的物体列表并自主绘制。\n五：纹理压缩 在游戏引擎中，图片不是以诸如 PNG、JPEG 等格式存在的，因为这些压缩方式并不支持随机访问，并且计算的复杂度非常高\n实际用到的压缩技术是——Block Compression：\n将一张图片分为多个色块（比如 4 x 4），对于每一个色块，只存储其中的像素的最大值与最小值，而其他像素可以认为是这两个像素值插值得到的结果\n在 PC 上：有 BC7（modern）、DXTC（old）等\n在移动端上：有 ASTC（modern）、ETC / PVRTC（old）等\n六：建模工具 多边形建模、雕刻、扫描、程序化建模\u0026hellip;..\n七：新的模型管线 Cluster-Based Mesh Pipeline 核心思想是放弃以“单个三角形”或“整个模型（Mesh）”为单位的传统处理方式，改由将模型切分成固定大小的“网格簇（Cluster）”作为基础计算单元，并完全由 GPU 驱动进行并行渲染。\nNanite 相当于是 Cluster-Based Mesh Pipeline 的更进一步\n原文：渲染实践\n","permalink":"https://jiat-t.github.io/graphics/games104-rendering-practice/","summary":"整理 GPU 执行架构、渲染资源组织、可见性裁剪、纹理压缩与 Cluster/Nanite 模型管线。","title":"GAMES104：渲染实践"},{"content":"最推荐你做的展示项目\n我建议你直接做一个仓库或 UE 项目，名字可以叫：\nUE5-Material-Lab\n内容不要太大，但结构要专业：\n01_PBR_MasterMaterial 02_NPR_ToonCharacter 03_PostProcess_Outline 04_Procedural_Surface 05_Runtime_Material_Control 06_Material_Optimization 每个 demo 都要有：\n效果截图或短视频 材质图截图 关键 Material Function 说明 参数面板截图 性能视图截图 简短 README：目标、实现、关键节点、性能注意点 这样别人看你的项目时，不会觉得你只是“会用 UE 做材质”，而会看到你掌握了材质系统的工程化组织。\n几个具体 demo 方向\nPBR Master Material：\n一个支持 packed ORM、normal map、detail normal、emissive、clear coat、wetness、vertex color blend 的主材质。重点展示参数体系和实例复用。\nNPR Character Shader：\nramp diffuse、step specular、rim light、matcap、outline。可以结合你之前 PGR/Unity 角色渲染经验迁移到 UE5。\nPost Process Outline：\n用 depth / normal / custom stencil 做边缘检测。重点写清楚 SceneTexture、CustomDepth、Blendable Location。\nProcedural Surface：\n雪地、湿润、苔藓、沙地、岩石高度混合。重点是 world position、noise、triplanar、height blend。\nRuntime Material Control：\nC++ 或 Blueprint 控制Material Instance Dynamic，比如受伤变色、溶解进度、扫描线、雨天 wetness。这个能连接你说的“C++ 与引擎交互”。\nOptimization Case Study：\n同一个材质做高/低两个版本，对比 texture samples、instruction count、shader complexity、static switch。这个非常加分，因为它体现工程判断。\n原文：UE5材质备忘录\n","permalink":"https://jiat-t.github.io/ue/ue5-material-reference/","summary":"整理主材质、风格化着色、后处理描边、程序化表面、运行时材质控制与性能对比的练习方向。","title":"UE5 材质备忘录"},{"content":"Vulkan 的核心思想是“显式（Explicit）”，这意味着任何底层资源和状态都必须亲手创建和绑定。\nVulkan 初始化核心流程 整个初始化工作，实际上就是为了建立起程序与显卡之间顺畅的通信管道。主要分为以下 5 大核心步骤：\n1. 创建 Vulkan 实例 (VkInstance) 这是应用程序与 Vulkan 运行时（Runtime）建立联系的起点。\n需要做什么： 填写应用信息（VkApplicationInfo），并开启你需要的全局扩展（Extensions）（如与窗口系统交互的 GLFW 扩展）和全局校验层（Validation Layers）。\n校验层（Validation Layers）： Vulkan 本身为了性能几乎不做错误检查，校验层就像一个贴身的“纠错老师”，会在调用 API 不规范时在控制台疯狂报错，是开发时的救命稻草。\n2. 设置调试回调 (VkDebugUtilsMessengerEXT) 光开启校验层还不够，还得在代码里注册一个“监听器”，把校验层抓到的错误和警告信息重定向到你的控制台输出。\n注意点： 因为这个功能属于扩展（Extension），它的创建函数指针需要通过 vkGetInstanceProcAddr 动态加载。 3. 创建窗口表面 (VkSurfaceKHR) Vulkan 是跨平台的，它本身不关心你是 Windows、Linux 还是 Android。为了把渲染结果显示到屏幕上，需要创建一个连接 Vulkan 与原生窗口系统（OS Window System）的桥梁。\n通常做法： 直接调用 GLFW 的 glfwCreateWindowSurface，它会处理好底层繁琐的平台特异性代码。 4. 挑选物理设备 (VkPhysicalDevice) 有了实例和表面后，就需要去系统里翻箱倒柜，看看电脑里有几张显卡，并挑出一张最合适的。\n检查设备属性与特性： 检查 GPU 是否支持需要的功能（比如几何着色器、光追等）。\n检查队列族（Queue Families）： 这是关键！需要检查显卡是否支持图形操作（Graphics）以及是否支持将图像呈现（Present）到刚刚创建的窗口表面上。\n5. 创建逻辑设备 (VkDevice) 与获取队列 (VkQueue) 物理设备（VkPhysicalDevice）只是一个“只读”的显卡硬件描述，不能直接操作它。因此需要基于它创建一个逻辑设备，作为后续所有渲染业务的核心接口。\n激活设备扩展： 在这里要明确开启交换链扩展（VK_KHR_swapchain），这是后续把画面呈现在屏幕上的核心扩展。\n获取队列句柄： 逻辑设备创建成功后，从中提取出具体的图形队列（Graphics Queue）和呈现队列（Present Queue）的句柄。后续所有的渲染命令、提交操作，都是往这两个队列里“扔任务”。\n一个生动的比喻 如果把 Vulkan 渲染比作开一家跨国物流工厂，那第一章的初始化其实就是在做工商登记和招募核心团队：\nVkInstance： 公司的营业执照。没有它，你属于非法经营，系统不认你。\nVkDebugUtilsMessenger： 公司的合规风控部门。专门盯着员工有没有违规操作，有就立刻报警。\nVkSurfaceKHR： 工厂的大门/出货码头。决定了你的货物（像素）最终要运到哪个市场上。\nVkPhysicalDevice： 去人才市场挑员工。看看哪个应聘者（显卡）有腱子肉、会开卡车。\nVkDevice： 跟挑中的员工签劳动合同。明确规定他每天干多少活，有哪些特殊福利。\nVkQueue： 员工屁股底下的流水线/办公桌。你以后把一箱箱的货物（命令缓冲区）砸到这个桌子上，他就会开始疯狂干活。\n","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/initialization/","summary":"整理 Vulkan 初始化中的实例、调试回调、窗口表面、物理设备、逻辑设备与队列。","title":"Vulkan 初始化核心流程"},{"content":"在讲队列族（Queue Family）之前，我们可以用一个非常形象的现实生活场景来理解它：\n想象一下，GPU 是一个大型的高效工厂，而队列族就是这个工厂里的不同车间。\n车间 A（图形渲染车间）： 里面有一批专门负责涂色、画画的工人，也配有搬运工。\n车间 B（纯数据搬运车间）： 里面全是搬运工，专门负责把原材料（数据）从外面的仓库（CPU/内存）搬进工厂内部（显存）。\n每个车间里的工人，就是一条条具体的队列（VkQueue）。你把任务（Command Buffer）写在单子上，丢给某一个车间的某一个工人，他们就会开始排队干活。\n在 Vulkan 中，队列族（Queue Family） 就是对具有相同硬件功能、共享相同硬件特质的一组队列的抽象分类。\n分类是按“族（Family）”来分的：图形是一族，计算可能是一族，传输可能是一族。 “队列（Queue）”是族里面的具体执行成员：同一个族里的所有队列，功能是完全一模一样的。 1. 为什么 Vulkan 要引入“族”的概念？ 传统的图形 API（如 OpenGL）通常隐式地提供一个“大总管”上下文，不管下达什么指令它都吞下去，但我们根本不知道 GPU 底层是谁在干活。\n而 Vulkan 为了追求极致的性能和多线程掌控力，把 GPU 的底层硬件底牌彻底掀开了：\nGPU 芯片内部其实是由不同的物理硬件引擎（Execution Engines）组成的。有的硬件电路专门跑光栅化，有的专门跑通用计算，有的专门做 DMA 内存传输。\nVulkan 为了能压榨这些硬件，它把这些互相独立、功能不同的硬件执行管道分组，每一组就是一个 Queue Family。\n2. 队列族的四大核心属性 当用代码去枚举并检查显卡的队列族时（通过 vkGetPhysicalDeviceQueueFamilyProperties），每一个队列族都会报告它具备以下几个核心属性：\n① 队列标志 (queueFlags) 这决定了这个车间能干什么活。它是以下几个标志位的组合：\nVK_QUEUE_GRAPHICS_BIT：能处理图形管线任务（画画）。\nVK_QUEUE_COMPUTE_BIT：能处理通用计算（算物理、算AI）。\nVK_QUEUE_TRANSFER_BIT：能高效搬运数据（内存拷贝）。\nVK_QUEUE_SPARSE_BINDING_BIT：能处理虚拟/稀疏内存绑定。\n② 队列数量 (queueCount) 也就是这个车间里有多少个“打工人”（实例）。\n比如某个图形队列族的 queueCount = 1，说明这个车间虽然能画画，但只有一条流水线，你只能实例化出一个 VkQueue 句柄来用。如果 queueCount = 4，就可以同时创建 4 个独立的 VkQueue 句柄，从多个 CPU 线程同时往这 4 个队列里提交任务，实现真正的多线程并行提交。\n③ 时间戳支持精度 (timestampValidBits) 这个队列族在执行性能分析（Profiling）时，支持多少位的计时器精度。如果值为 0，说明该队列族不支持时间戳查询。\n④ 图像传输的最小粒度 (minImageTransferGranularity) 该队列族在搬运图像数据时，支持的最小对齐宽高（比如 $1 \\times 1 \\times 1$ 或者 $4 \\times 4 \\times 1$）。\n3. 实际硬件中，队列族长什么样？ 硬件厂商（NVIDIA, AMD, Intel）在底层设计上各有想法，这也导致了在不同的显卡上，队列族的分布截然不同。\n我们可以看看两种最典型的显卡队列族分布（假设我们要找图形、计算、传输三种功能）：\n情况 A：全能型“大车间”（常见于桌面离散显卡，如 NVIDIA） 有些显卡非常简单粗暴，它可能只提供 1 到 2 个队列族：\nQueue Family 0: queueCount = 1, 标志位是 GRAPHICS | COMPUTE | TRANSFER。这个族是个全能车间，什么都能干。\nQueue Family 1: queueCount = 1, 标志位只有 TRANSFER。这是一个纯搬运工车间。\n在这种显卡上，图形任务、计算任务往往都是找 Family 0 创建的同一个 VkQueue 去排队执行。\n情况 B：专职型“独立车间”（常见于支持强异步计算的显卡，如 AMD） 有些显卡为了追求极致的异步并行（Async Compute），会分得很细：\nQueue Family 0 (Graphics): queueCount = 1，带 GRAPHICS | COMPUTE | TRANSFER。\nQueue Family 1 (Compute): queueCount = 2，只带 COMPUTE | TRANSFER。没有图形能力！\nQueue Family 2 (Transfer): queueCount = 2，只带 TRANSFER。纯 DMA 拷贝引擎！\n在现代游戏引擎中，这种架构就非常爽。我们可以让主线程把渲染画面的命令丢给 Family 0；同时让另一个后台线程把粒子物理模拟计算丢给 Family 1 的独立计算队列。两边在 GPU 硬件层面上是同时并行跑的，互不干扰，这就叫异步计算（Async Compute）。\n总结 在 Vulkan 的初始化流程中，必经之路是：\n遍历所有可用的物理显卡。\n遍历该显卡上的所有 队列族（Queue Family）。\n挑选出满足你需求的族（比如既支持 GRAPHICS 又能支持窗口 PRESENT 的族）。\n记住这个族的 索引值（Queue Family Index）。\n在创建逻辑设备（VkDevice）时，指定在这个族里创建几个具体的 队列（VkQueue） 供以后干活使用。\n","permalink":"https://jiat-t.github.io/vulkan/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/queuefamily/","summary":"用硬件执行能力和现实类比梳理 Vulkan Queue Family 与 VkQueue 的关系。","title":"Vulkan 队列族"},{"content":"在 Vulkan 中，Window Surface（窗口表面，通常对应句柄 VkSurfaceKHR） 是一个连接 Vulkan 图形管道 与 操作系统的窗口系统（OS Window System） 的跨平台抽象层桥梁。\n简单来说：Vulkan 本身是一个极其纯粹的硬件渲染 API，它甚至不知道屏幕、窗口、或者“显示”是什么。它只负责在显存里往一片内存（Image）里涂颜色。\n而 Window Surface 就是我们向操作系统申请的一块“画布”，有了它，Vulkan 才知道应该把显存里画好的图像投递到屏幕的哪个具体窗口上。\n1. 为什么不能直接画在窗口上？（核心痛点） 在老旧图形 API 比如 OpenGL 中，API 会隐式地处理与操作系统的对接。但 Vulkan 为了追求绝对的平台无关性，把底牌彻底掀开了：\nVulkan 的世界： 只有物理设备、逻辑设备、命令缓冲区、渲染管线、显存。 操作系统的世界： Windows 认的是 HWND（窗口句柄）和 HDC；Linux 认的是 Window 变体（X11 或 Wayland）；macOS 认的是 CAMetalLayer。 这两者由于设计哲学完全不同，根本无法直接对话。\nVkSurfaceKHR 就是那个连接两者的中间人。 它把操作系统的原生窗口句柄（比如 Windows 的 HWND）包装起来，变成一个通用的、Vulkan 认识的对象。\n2. 名字后面的 KHR 代表什么？ 如果仔细观察，会发现核心的 Vulkan 对象（如 VkDevice、VkImage）后面都没有后缀，而 VkSurfaceKHR 后面跟着 KHR。\n其中，KHR 代表 Khronos 官方扩展（Window System Integration, WSI）。\n因为并不是所有的 Vulkan 程序都需要显示器。比如运行在高性能服务器上的 AI 训练、离线光线追踪渲染器，它们只需要把渲染结果保存为图片，不需要弹出一个窗口。 因此，窗口显示功能被剥离出了核心规范，放进了扩展里。VkSurfaceKHR 就是由全局扩展 VK_KHR_surface 引入的。 3. Window Surface 是如何创建的？ 由于每个操作系统的窗口系统底层实现完全不同，Vulkan 官方针对不同平台提供了专属的“特化创建函数”。\n如果纯手写跨平台代码，需要写一堆宏条件编译：\n1#if defined(VK_USE_PLATFORM_WIN32_KHR) 2 // Windows 专用的创建表单 3 VkWin32SurfaceCreateInfoKHR createInfo{}; 4 createInfo.hwnd = hwnd; // 传入 Win32 的窗口句柄 5 vkCreateWin32SurfaceCreateInfoKHR(...); 6 #elif defined(VK_USE_PLATFORM_XLIB_KHR) 7 // Linux X11 专用 8 VkXlibSurfaceCreateInfoKHR createInfo{}; 9 vkCreateXlibSurfaceKHR(...); 10#endif C++ 现代工程实践（配合 GLFW） 但是现在几乎没有人会去纯手写上面那些恶心的平台宏。我们通常会使用 GLFW。GLFW 会在内部自动判断当前的操作系统，只需要一行代码，它就能把 VkSurfaceKHR 完美创建出来：\n1VkSurfaceKHR surface; 2// GLFW 内部会自动调用 vkCreateWin32SurfaceKHR 或 vkCreateXlibSurfaceKHR 3if (glfwCreateWindowSurface(instance, window, nullptr, \u0026amp;surface) != VK_SUCCESS) { 4 throw std::runtime_error(\u0026#34;Failed to create window surface!\u0026#34;); 5} 4. 拥有 Surface 之后，下一步做什么？ 创建好 Window Surface 只是第一步。在整个 Vulkan 初始化生命周期里，它是后续两个极为核心的步骤的强依赖参数：\n① 筛选硬件队列族（Present Support Check） 我们在聊“呈现队列”时提过，你需要拿着这个创建好的 surface 去质问物理显卡：“你的哪个队列族，能把画画好之后的图像安全地扔进这个 surface 里显示出来？”\n1VkBool32 presentSupport = VK_FALSE; 2vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, queueFamilyIndex, surface, \u0026amp;presentSupport); ② 创建交换链（Swapchain） 这是 Surface 最重要的归宿。有了 Surface，你才能向显卡申请交换链（Swapchain）。\n交换链会在这个 Surface 背后开辟一个“前台缓冲区”和“后台缓冲区”（通常是 2~3 张图形图片，即双重缓冲或三重缓冲）。GPU 在后台缓冲区拼命画画，画完之后，通过交换链，把图片平铺到这个 Surface 上展示给玩家看。\n总结 Window Surface（VkSurfaceKHR） 不是一块真实的显存，也不是一个可以往里写数据的缓冲区。 它是一个抽象的“显示表面占位符”。 它的唯一任务，就是把操作系统的原生窗口死死地和 Vulkan 的世界锚定在一起。后续所有关于屏幕刷新率、垂直同步、交换链双缓冲、窗口缩放格式的配置，全部都要基于这个 Surface 来展开。 原文：Window Surface\n","permalink":"https://jiat-t.github.io/vulkan/vulkan-window-surface/","summary":"整理 VkSurfaceKHR 的窗口系统职责、GLFW 创建流程，以及呈现队列检查和交换链之间的关系。","title":"Vulkan Window Surface"},{"content":"在图形学开发中，GLFW 是一个非常轻量级、跨平台的 C 语言开源库。它的核心任务非常纯粹：帮你处理窗口创建、操作系统上下文管理以及用户输入。\n如果要写 Vulkan、OpenGL 或 OpenGL ES 程序，不能直接让代码在空无一物的操作系统上画画，必须先向 Windows、Linux 或 macOS 申请一个“窗口”，并捕获键盘鼠标的操作。GLFW 就是干这个脏活累活的。\n1. 为什么需要 GLFW？（解决跨平台痛点） 如果不使用任何第三方库，纯手写一个能在屏幕上弹出来的窗口，代码会变成这样：\n在 Windows 上： 调用 Win32 API，写一堆 WNDCLASSEX、CreateWindowEx，还要写一个极其繁琐的 WindowProc 消息循环来处理 Windows 系统的各种消息。 在 Linux 上： 得去和 X11 (Xlib) 或者更现代的 Wayland 协议打交道，处理各种底层的 Display 和 Window 句柄。 在 macOS 上： 得用 Objective-C 或 Swift 去调用 Cocoa 框架。 这会导致图形引擎还没开始写画三角形的代码，就已经被不同操作系统的平台初始化代码淹没了。\nGLFW 的出现，把这些平台差异完全抹平了。 无论在什么操作系统上，创建窗口的代码都精简成了统一的一行：glfwCreateWindow。\n2. GLFW 的核心职能 GLFW 的功能非常克制和专一，它只管窗口和输入，不管怎么画画：\n窗口管理 (Window Management)： 创建和销毁桌面窗口、处理窗口缩放、最大化/最小化、移动、多显示器全屏切换等。 图形上下文管理 (Context Management)： 配置和初始化 OpenGL/OpenGL ES 的上下文，或者专门为 Vulkan 提供跨平台的 Surface 创建支持。 输入事件处理 (Input Handling)： 极度简单的回调机制。可以轻松捕获键盘按键（按下、抬起、连发）、鼠标移动、鼠标点击、滚轮滚动，甚至还支持手柄/摇杆（Joystick）的输入。 高精度计时器： 提供了一个跨平台的高精度时间函数 glfwGetTime()，非常适合用来计算游戏引擎的帧间隔时间（Delta Time）。 3. 它在 Vulkan 开发中的特殊生态位 如果是在写 OpenGL，GLFW 会顺便创建好 OpenGL 的渲染上下文（Context）。但在 Vulkan 中，情况发生了一点变化。\nVulkan 是一个完全隐式无关的 API，它默认不感知窗口系统。我们在前面聊过，Vulkan 要想把画面扔到屏幕上，必须借助 WSI（窗口系统集成）扩展（比如 VK_KHR_surface）。\n为了创建这个 VkSurfaceKHR，如果纯手写，需要针对 Windows 调用 vkCreateWin32SurfaceKHR，针对 Linux 调用 vkCreateXlibSurfaceKHR。\n而 GLFW 为 Vulkan 提供了极好的原生支持，它直接提供了两个极简的 API，完美融入 Vulkan 初始化工作流：\n① 剥离 OpenGL 上下文 在创建窗口前，通过设置 Hint 告诉 GLFW：“我不需要你帮我初始化 OpenGL，我是给 Vulkan 用的”：\n1glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 2GLFWwindow* window = glfwCreateWindow(800, 600, \u0026#34;Vulkan Window\u0026#34;, nullptr, nullptr); ② 自动获取 Instance 级别扩展 我们在初始化 Vulkan VkInstance 时，需要开启平台特定的窗口扩展。GLFW 可以直接把当前操作系统需要的所有全局扩展名称打包：++\n1uint32_t glfwExtensionCount = 0; 2const char** glfwExtensions; 3glfwExtensions = glfwGetRequiredInstanceExtensions(\u0026amp;glfwExtensionCount); 4 5// 随后直接把 glfwExtensions 塞进 VkInstanceCreateInfo 的 enabledExtensionNames 中 ③ 一键创建跨平台 Surface 当选好显卡，准备把窗口和 Vulkan 绑定时，一行代码就能搞定原本需要分平台宏判断（#ifdef _WIN32）的 Surface 创建工作：\n1VkSurfaceKHR surface; 2if (glfwCreateWindowSurface(instance, window, nullptr, \u0026amp;surface) != VK_SUCCESS) 3{ 4 throw std::runtime_error(\u0026#34;failed to create window surface!\u0026#34;); 5} 4. GLFW vs SDL2 vs GLUT (横向对比) 当然还有其他类似的库，它们之间的侧重点各有不同：\n库名称\n特点与定位\n适用场景\nGLFW\n极其现代、轻量、纯粹。 只专注于窗口和输入，API 设计非常符合现代 C/C++ 程序员的习惯。没有臃肿的历史包袱。\n现代图形学学习、自研现代渲染器（Vulkan/OpenGL）、游戏引擎底层窗口封装。\nSDL2 / SDL3\n巨无霸级别的多媒体库。 除了窗口和输入，它还自带音频播放、线程管理、文件 IO、甚至自带一个 2D 渲染引擎。\n跨平台商业游戏开发、模拟器开发（因为它管得极宽）。\nGLUT / FreeGLUT\n古老、过时的化石库。 控制权在库本身（回调地狱），很难和现代渲染管线及多线程很好地融合。\n工业界和现代开发已基本淘汰，仅在一些高校非常老旧的 OpenGL 1.x/2.x 课设中能看到。\n总结 GLFW 是现代图形学开发的“标准敲门砖”。它就像一顶安全帽，把各家操作系统底层最恶心、最繁琐的窗口创建和消息循环脏活全部挡在外面，让我们能够把 100% 的精力优雅地集中在 Vulkan 队列族、交换链、着色器和渲染管线的编写上。\n原文：GLFW\n","permalink":"https://jiat-t.github.io/vulkan/vulkan-glfw/","summary":"整理 GLFW 的跨平台窗口与输入职责，以及 Vulkan 实例扩展查询和窗口 Surface 创建方式。","title":"GLFW 与 Vulkan 窗口创建"},{"content":"在 Vulkan 中，VkResult 是一个非常核心的枚举类型（Enumeration），专门用来作为绝大多数 Vulkan 函数的返回值。\n因为 Vulkan 是一个极度显式的低级 API，驱动程序内部几乎不会帮你做任何隐式错误处理，也不会抛出 C++ 异常。任何一个操作（比如创建对象、分配显存、提交命令）成功与否，都必须通过 VkResult 显式地反馈给开发者。\nVkResult 的两大分类 VkResult 的枚举值主要分为两大阵营：成功/状态码（Success/Status Codes） 和 错误码（Error Codes）。\n核心成功码（通常 ≥ 0） 这些值代表函数不仅成功执行，还可能带回了一些运行时状态信息：\nVK_SUCCESS (0): 最完美的情况，函数成功执行且没有发生任何意外。 VK_NOT_READY: 一个围栏（Fence）或查询还没有完成。 VK_TIMEOUT: 某个等待操作（比如等待一个信号量或围栏）在指定时间内没有完成。 VK_INCOMPLETE: 缓冲区太小，无法返回所有请求的结果（例如获取交换链图像时，提供的数组长度不够，只返回了一部分）。 核心错误码（通常 \u0026lt; 0） 这些值代表操作彻底失败。由于 Vulkan 遵循 RAII（资源获取即初始化）和严苛的显式内存管理，很多错误都直接指向内存和驱动底层：\nVK_ERROR_OUT_OF_HOST_MEMORY: CPU 端（主机）内存不足，无法完成分配。 VK_ERROR_OUT_OF_DEVICE_MEMORY: GPU 端（设备）显存不足。这是开发中非常需要警惕的错误，通常需要你重新规划 VkDeviceMemory 的分配策略。 VK_ERROR_INITIALIZATION_FAILED: 初始化失败，通常是因为显卡驱动不支持 Vulkan 或者是某些层（Validation Layers）没找对。 VK_ERROR_DEVICE_LOST: 极其致命的错误。GPU 挂起了、掉线了、或者因为耗时过长被操作系统强制重置了（TDR）。 VK_ERROR_OUT_OF_DATE_KHR: 交换链（Swapchain）特有错误。当窗口大小改变（比如用户拖动了窗口）或者屏幕旋转时，原有的交换链图像与当前的 Surface（表面）不再匹配，必须重建交换链。 总结 把 VkResult 看作是 Vulkan 驱动与你的 C++ 代码之间沟通的“体检报告”。通过它，你能第一时间抓到是 CPU 内存爆了、GPU 显存穿了、还是用户偷偷拉大窗口了。\n原文：VkResult\n","permalink":"https://jiat-t.github.io/vulkan/vulkan-vkresult/","summary":"整理 VkResult 的成功与状态码、常见错误码及 Vulkan 调用中的返回值检查。","title":"Vulkan VkResult"},{"content":"【Reference】： https://zhuanlan.zhihu.com/p/349594815\n渲染中的加速结构大致可以分为两类，一种是基于物体的加速结构，一种是基于空间的加速结构，本质上都是为了避免每条光线、每块像素、每个物体都与整个场景进行暴力测试。\n一：基于物体的加速结构 传统的包围球 / 包围盒结构 多用于视锥剔除与碰撞检测，而在实际渲染中并不常用，因为球体或任意朝向盒的求交运算远比 AABB 复杂。\n层次包围盒（BVH, Bounding Volume Hierarchy） 最常用的加速结构，本质上是一棵基于物体的划分树（一般是二叉树，但也可以是四叉、八叉等）。\n假设现在需要对场景中的所有三角形进行求交，最暴力的方法是逐个进行测试，时间复杂度是 $O(n)$；如果使用 BVH 进行测试，平均情况下可以降低到接近 $O(\\log n)$。\n在构建时，首先计算整个场景的包围盒（通常是 AABB，轴对齐包围盒），然后将物体（三角形等）分成两组，分别计算它们的包围盒作为子节点 / 左右子树，以此类推，直到叶子节点包含极少数的三角形。\nBVH 的结构 根节点：一个囊括了整个场景的巨大包围盒。 中间节点：仍然是一个包围盒，但是相对于父节点来说，细分层级更高（包围盒更小），盒内包含的面片数量更少，同时还包含两个指向子节点的指针。 叶子节点：一个包围盒，以及指向实际几何数据的指针 / 索引（比如两个三角形）。 BVH 的遍历流程 从栈顶取出一个 AABB 与光线进行求交。 如果没有相交：直接 continue 跳过，进入下一轮循环。 如果存在交点：返回进入该 AABB 的最短距离 $t_{enter}$。 将 $t_{enter}$ 与之前记录的 $t_{max}$ 进行比较，进行剪枝。 如果 $t_{max} \\le t_{enter}$，就说明当前这个 AABB 被挡住了，光线不应该看到它，因此直接舍弃。 反之，则进入这个 AABB。 进入 AABB 后，还需要根据节点类型进行不同的处理。 叶子节点：直接对里面包含的所有三角形执行求交，并根据返回的距离判断是否更新 $t_{max}$。 中间节点：光线分别与左右两个 AABB 求交，并得到与两个盒子的距离，将距离较远的盒子压入栈底，距离较近的盒子压入栈顶。 缺点：多个包围盒可能出现重叠的情况（如图），此时单单只对一个包围盒执行求交操作是不够的，因为并不清楚哪个包围盒在前，所以需要对左右两棵子树都执行求交，这样才可以确定下一步要进入哪个包围盒。\n二：基于空间的加速结构 KD-Tree（K-Dimensional Tree） KD-Tree 的核心思想是：不断地用一个轴平面对空间进行分割，逐步将场景划分为更小的区域；光线只需要检查它所经过的区域内的物体，而不是所有三角形。\nKD-Tree 的结构 根节点：整个场景的包围盒。 中间节点：记录当前节点的划分轴与划分位置。 叶子节点：代表当前 kd-tree 的最小层级区域，包含此区域内所有三角形数据的指针。 如果一个物体同时处于多个区域，那么每个区域对应的叶子节点中都会存在这个物体的指针。\nKD-Tree 的遍历流程 光线首先与根节点相交，计算与划分平面的交点的距离。 case 1：光线只击中近端的空间，只递归遍历近端子树。 case 2：光线只击中远端的空间，只递归遍历远端子树。 case 3：光线先穿过近端，再穿过远端，先将远端子树压入栈中，接着递归遍历近端子树。 优点：相较于 BVH，KD-Tree 的计算量更小，因为 KD-Tree 的遍历遵循“由近及远”原则，所以光线一旦与三角形相交，且光源与交点的距离小于与远端的距离，就可以确定这就是最近交点；而 BVH 由于可能存在重叠的情况，即使在左子树中找到了交点，也无法确定这就是最近交点，仍然需要将右子树也遍历一遍。\n缺点：内存开销不可控；构建速度极慢（无法实现动态重建）；硬件访问不友好。\n八叉树（Octree） 核心逻辑：在三维空间中，每一次切分都需要沿着 x 轴、y 轴、z 轴各切一刀，将其均分为八块。\nOctree 的结构 根节点：一个包裹着整个场景的严格立方体。 中间节点：含有 8 个指向子节点的指针。 叶子节点：不可再分的最小立方体单元，它可以包含其中所有的三角形数据的指针，也可以作为体素直接参与体素渲染。 八叉树一般不用于光线与物体求交的加速，因为它的划分实在是太过规整了，面对复杂 / 动态的几何体时会非常吃力（比如可能出现所有三角形全部堆积在一个小立方体内的情况）；但是在全局光照（如 SVOGI 等）、视锥剔除领域，八叉树应用得非常广泛。\n均匀网格（Uniform Grid） 核心思想：抛弃树形结构，直接将场景均分为若干个完全相同的格子。\nUniform Grid 底层的数据结构非常简单，只有两个数组：\n网格标头数组：一个一维数组，长度严格等于 $N_x \\times N_y \\times N_z$（即三维分辨率的乘积）。每个元素非常简单，只存一个整数，指向“物体列表数组”的起始索引。 物体列表数组：一个连续的内存块，按顺序存放每个格子里包含的三角形（或粒子）的 ID。 因此对于场景中的每一个点，如果想要知道自身处于哪一个格子，都可以在 $O(1)$ 的时间复杂度内对其进行查询；之后进行光线求交时，Uniform Grid 会采用 3D DDA 算法像走棋盘一样沿着光线方向逐个推进格子，每进入一个格子，光线就会与该格子里记录的三角形进行求交测试。\n由于网格是严格由近及远推进的，一旦在当前格子内找到了交点，算法就可以立刻提前终止并返回结果，从而直接跳过后面所有剩余的格子。\n既然构建简单（平铺直叙）、定位 $O(1)$、遍历无栈，为什么现代光线追踪不用它？\n图形学中有一个著名的黑话叫 “茶壶与体育场（Teapot in a stadium）”问题。它精确地命中了均匀网格的死穴：\n假设你的游戏场景是一个巨大的足球场（空旷空间），而在球场正中心放了一个做工极其精细的茶壶（包含 100 万个微小三角形）。\n如果你要用均匀网格来加速这个场景，你会陷入两难的绝境：\n如果格子定得太大（网格太稀疏）：整个球场可能就被分成了 4 x 4 x 4 个格子。那么中心的那个格子会把整个茶壶的 100 万个三角形一股脑全部吞进去。当光线射中这个中心格子时，退化成暴力求交，网格形同虚设。\n如果格子定得太小（网格太精密）：为了把茶壶表面的三角形分干净，你把格子大小设为 1 毫米。这意味着周围空旷的足球场空间也被切成了几百亿个 1 毫米的空格子。\n内存崩塌：海量的空格子会瞬间吃光所有的显存。\n遍历雪崩：一束光线从球场的一端射向另一端，它不得不无意义地执行几万次 DDA 循环去“走格子”（哪怕里面全是空气），渲染速度直接跌入谷底。\n因此，Uniform Grid 通常只用于物理仿真、流体模拟、粒子碰撞等方面。\n附：现代实时光线追踪中的加速结构 在 DirectX Raytracing、Vulkan RT 等 API 中，为了在 GPU 上实现每秒数百万甚至更多次的光线求交，使用的都是 BLAS + TLAS 的两层 BVH 架构。\n1. BLAS（Bottom-Level Acceleration Structure，底层加速结构） BLAS 针对的是每一个独立模型内部的三角形，与上文所说的 BVH 基本相同；相较于 TLAS，BLAS 的构建成本更高，通常会采用 SAH 算法，其内部存储的是真实的顶点与三角形数据。\n2. TLAS（Top-Level Acceleration Structure，顶层加速结构） TLAS 内部不包含任何三角形，只有不同的 BLAS 实例，以及每个实例各自在世界空间中的变换矩阵、材质 ID、遮罩等；由于 TLAS 的大小仅仅只和场景中的物体个数相关（通常几千到几万个），数据量较小，所以 TLAS 的构建速度远快于 BLAS，也就可以在每帧进行重建。\n保存 BLAS 的世界矩阵的原因\n正因为 BLAS 的构建速度较慢，因此难以实现每帧进行重建，所以采用“空间换时间”的策略，即将每个实例的世界矩阵保存下来。之后当画面运动时，只需要通过前一帧的世界矩阵反推出模型空间坐标，再使用当前帧的世界矩阵即可实现在不重建 BLAS 情况下的空间变换。\n","permalink":"https://jiat-t.github.io/posts/acceleration-structure/","summary":"整理 BVH、KD-Tree、八叉树、均匀网格以及实时光线追踪中的 BLAS/TLAS 加速结构。","title":"加速结构"},{"content":"这个项目面试时可以按“基础路径追踪 -\u0026gt; 资产/几何 -\u0026gt; PBR -\u0026gt; IBL/MIS -\u0026gt; 工程性能”的顺序讲。\n1. 基础路径追踪架构\n核心知识点：Ray、HitRecord、Camera、Material、递归路径追踪讲解：路径追踪的核心是从相机发射射线，找到最近交点，根据材质生成下一条采样射线，递归累积直接光、间接光和自发光。此项目中的实现：Camera.h:413 负责 ray_color() 递归积分；Ray.h 表示射线；Hittable.h 用 HitRecord 保存交点、法线、材质、UV、TBN 等信息。一句话总结：这个项目的底层是一套标准 CPU 路径追踪框架，核心链路是“相机射线 -\u0026gt; 求交 -\u0026gt; 材质采样 -\u0026gt; 递归估计辐射”。\n2. 几何与 BVH 加速\n核心知识点：Sphere、Triangle、Quad、AABB、BVH讲解：路径追踪如果每条光线遍历所有物体，复杂度很高，所以需要用包围盒和 BVH 把求交范围快速缩小。此项目中的实现：BVH.h:7 用 BVH_Node 递归构建左右子树；AABB.h 做包围盒测试；Triangle.h:95 还实现了三角形光源采样所需的 pdf_value() 和 random()。这部分对应早期 PR #18：三角形 PDF / random sampling 和 WaveFront OBJ loader。一句话总结：BVH 是这个离线渲染器能渲染网格模型的基础性能结构。\n3. 材质抽象\n核心知识点：Scatter / Eval / PDF讲解：面试里这点很重要。不要只说“材质会反射光”，要说你把材质拆成三件事：Scatter 决定怎么采样下一条方向，Eval 计算这个方向上的 BRDF 值，PDF 返回这个采样方向的概率密度。三者必须一致，否则渲染会有偏。此项目中的实现：Material.h:26 的 Material 基类提供 Scatter()、Eval()、PDF()；传统材质包括 Lambertian、Metal、Dielectric、Diffuse_Light、isotropic，PBR 材质也接入同一套接口。一句话总结：这个接口设计让材质采样、BRDF 评估和概率密度可以统一进入路径积分器和 MIS。\n4. Russian Roulette\n核心知识点：路径终止、无偏估计、方差控制讲解：路径不能无限递归，简单用固定 max_depth 会截断能量；Russian Roulette 是在一定 bounce 后按概率终止路径，未终止的路径用生存概率补偿能量。面试里重点讲“它不是加速技巧本身，而是控制长路径计算成本，同时尽量保持无偏”。此项目中的实现：Issue #12 要求用 Russian Roulette 替代原来的固定深度；PR #20 引入基于材质反照率的动态生存概率，并配合 firefly clamping 控制低采样下的高亮噪点。一句话总结：Russian Roulette 让递归路径在深度较大时更经济，同时保留物理上合理的能量估计。\n5. OBJ / MTL 资产导入\n核心知识点：tinyobjloader、顶点/法线/UV、材质映射讲解：从硬编码球体过渡到真实模型，关键不只是读顶点，还要正确读面索引、UV、法线、材质 ID，并把 MTL 中的贴图路径映射到渲染器材质。此项目中的实现：ObjLoader.h:13 使用 tinyobjloader；ObjLoader.h:99 通过 material_id 找每个 face 的材质；ObjLoader.h:166 读取 diffuse / normal / roughness / metallic 贴图。对应 PR #28，目标是让 OBJ 材质能自动进入 PBR_Material。一句话总结：OBJ/MTL 这条链路让项目从“手写测试场景”升级成“可导入真实 PBR 资产”。\n6. 纹理与颜色空间\n核心知识点：sRGB、Linear、数据贴图讲解：PBR 项目里颜色空间很容易被问。baseColor 是颜色，要从 sRGB 转 linear；roughness、metallic、normal 是数据，不应该做 sRGB 变换。此项目中的实现：Texture.h:7 区分 color_space::SRGB 和 color_space::Linear；Texture.h:22 做 sRGB 到 linear；ObjLoader.h:177 明确 normal / roughness / metallic 作为 linear 数据贴图加载。一句话总结：正确的颜色空间处理是 PBR 结果可信的前提。\n7. Normal Map 与 TBN\n核心知识点：切线空间、TBN、shading normal / geometric normal讲解：normal map 的值在切线空间里，不能直接当世界空间法线用；需要三角形根据 UV 计算 tangent / bitangent，再和法线组成 TBN，把贴图法线变换到世界空间。还要区分几何法线和着色法线，避免求交偏移、正反面判断和视觉法线混在一起。此项目中的实现：Triangle.h:126 计算 tangent space；Triangle.h:212 同时保留 geo_n 和 n；Material.h:388 在 PBR_Material 中采样 normal map；ObjLoader.h:255 根据文件名判断 OpenGL / DirectX normal map 约定。对应 PR #27 和 Issue #29。一句话总结：normal map 不是简单改法线，而是 UV、TBN、几何法线和着色法线共同配合的资产链路。\n8. PBR 材质，非常重要\n核心知识点：metallic-roughness、GGX、Schlick Fresnel、Smith、能量守恒讲解：面试里可以这样说：我实现的是 metallic-roughness 工作流，baseColor 决定基础颜色，metallic 控制材质在电介质和金属之间过渡，roughness 控制微表面粗糙程度。镜面项用 GGX 描述微表面法线分布，Schlick Fresnel 描述视角越擦边反射越强，Smith 几何项描述微表面之间的遮挡。diffuse 和 specular 不是随便相加，而是根据 metallic 和 Fresnel 做能量分配，避免材质反射出超过入射光的能量。此项目中的实现：Material.h:203 是 PBR_Material；Material.h:227 的 Scatter() 建立 diffuse cosine PDF 和 GGX PDF 的混合采样；Material.h:246 的 Eval() 计算 PBR BRDF；Material.h:309 包含 GGX、Smith、Fresnel 组合。Issue #25 是 PBR 主目标，PR #26 和 PR #28 分别推进基础 PBR 与资产驱动 PBR。一句话总结：PBR 部分的重点不是“用了几个公式”，而是把材质参数、BRDF 评估、采样 PDF 和贴图工作流接成了一条一致的物理材质链路。\n9. GGX VNDF 采样\n核心知识点：NDF 采样 vs VNDF 采样、可见微表面讲解：普通 GGX 半程向量采样可能采到很多从当前视角不可见的微表面，尤其低 roughness 金属会浪费样本并产生高亮噪点。VNDF 采样会根据 view direction 采样“可见法线分布”，更适合光滑金属。此项目中的实现：PDF.h:101 是 GGX_PDF；PDF.h:107 标注其用于 PBR 的 VNDF specular PDF；PDF.h:169 通过 roughness space 拉伸视线方向并采样 visible-normal domain。PR #28 的评论记录里也有 “NDF -\u0026gt; VNDF” 的验证图。一句话总结：VNDF 采样是这个 PBR 材质在光滑金属上减少无效样本和 firefly 的关键优化。\n10. IBL，非常重要\n核心知识点：HDRI、经纬图、亮度重要性采样、solid-angle PDF讲解：IBL 不是只把环境贴图当背景。真正用于路径追踪时，环境图应该同时提供两件事：miss 时返回环境辐射；作为无穷远光源被显式采样。因为经纬图不是等面积映射，采样权重不能只看像素亮度，还要考虑球面面积权重，也就是靠近极点的 texel 在球面上面积更小。面试里要说清楚“亮度分布 + 球面面积权重 + PDF 转 solid angle”，不要只说“按亮的地方多采样”。此项目中的实现：Environment.h:23 是 LatLong_Environment；Environment.h:199 构建采样分布；Environment.h:261 用两级 CDF 采样 row / column；Environment.h:285 的 texel_weight() 使用 luminance 和 sin(theta)；Environment.h:73 把 UV PDF 转为 solid-angle PDF。一句话总结：这个项目的 IBL 重点是让 HDRI 从“可见背景”升级成“可被重要性采样的真实环境光源”。\n11. MIS，非常重要\n核心知识点：Light sampling、BSDF sampling、Power Heuristic讲解：直接光照有两个常见采样策略：从光源采样，或者从 BSDF 采样。光源采样适合面积光和环境高亮区域；BSDF 采样适合镜面/金属方向性很强的材质。MIS 不是二选一，而是把两个估计器按 PDF 可靠性加权组合，避免某个策略在特定场景下方差过高。此项目中的实现：Camera.h:18 有 power heuristic；Camera.h:436 构建 light PDF，把几何光和环境光合并；Camera.h:542 显式计算 light-side direct estimator；Camera.h:556 显式计算 BSDF-side direct estimator；Camera.h:641 单独处理 indirect continuation。对应 PR #30。一句话总结：MIS 是这个项目把 PBR、面积光和 HDRI 环境光稳定结合起来的核心采样框架。\n12. 第一跳 direct lighting 重构\n核心知识点：采样预算、direct / indirect 分离、emission guard讲解：第一跳对画面噪点影响最大，但如果每增加一个样本就递归整条路径，成本会爆炸。更好的做法是第一跳加强 direct lighting，间接路径仍保持单次 continuation。还要避免 direct lighting 已经显式估计了发光体后，递归又把同一个 emission 加一次。此项目中的实现：PR #30 把第一跳拆成 sample_direct_light_once()、sample_direct_bsdf_once()、sample_indirect_once()；Camera.h:496 的递归版本带 allow_emission；Camera.h:679 间接 continuation 关闭重复 emission。一句话总结：这一步体现的是渲染器工程优化思路：把采样预算集中用在最影响噪点的 direct term 上。\n13. 多线程，非常重要\n核心知识点：tile-based parallel rendering、std::execution::par、thread-local RNG、mutex / atomic讲解：路径追踪每个像素、每个 sample 基本独立，天然适合并行。但不能简单“多线程写图像”就完事，必须说明共享数据边界：每个 tile 写 framebuffer 的不重叠区域，所以像素写入不需要锁；进度计数用 atomic；预览窗口和日志刷新属于共享状态，要用 mutex 控制；随机数生成器必须 thread-local，否则多个线程共享 RNG 会产生数据竞争和采样相关性。此项目中的实现：Camera.h:114 定义 RenderTile；Camera.h:125 的 render_impl() 统一串行/并行渲染；Camera.h:219 用 std::for_each(std::execution::par, tiles.begin(), tiles.end(), render_tile) 调度；My_Common.h:29 使用 thread_local std::mt19937。benchmark：800x450 / 400 spp / depth 20，从 65.3296s 降到 29.9941s，约 2.178x。一句话总结：这个多线程实现的面试重点是“任务划分 + 共享数据隔离 + 同步边界”，而不是只说用了并行库。\n14. 实时预览窗口\n核心知识点：渲染过程反馈、preview buffer、UI 状态同步讲解：离线路径追踪时间长，实时预览能让用户看到 tile 渲染进度，但预览只应该读已经完成的 tile 数据，不能干扰渲染主流程。此项目中的实现：PPMPreviewWindow.h 负责窗口显示；Camera.h:276 把 tile 结果 blit 到 preview buffer；对应 Issue #23 和 PR #24。一句话总结：实时预览是工程可用性提升，不改变积分算法，但要求和多线程渲染正确同步。\n15. 展示场景与验证\n核心知识点：功能验证场景、benchmark、README showcase讲解：项目不只是堆功能，还要有能验证功能的场景：PBR 球、normal map 对比、OBJ PBR 自动映射、HDRI + area light 的 IBL/MIS 场景、README 综合展示场景。此项目中的实现：Renderer.cpp 中有 PBR_Test()、PBR_Benchmark()、PBR_Normal_Map_Test()、Obj_PBR_Test()、PBR_IBL_Test()、README_Showcase()。README 也明确列了当前能力和 benchmark 数据。一句话总结：这些场景是你面试时证明“功能真的跑通了”的证据链。\n面试总总结\n这个项目可以概括为：我从基础 CPU 路径追踪出发，先完成射线求交、材质递归、BVH、OBJ 加载和实时预览；随后把材质系统重构为 Scatter / Eval / PDF，接入 metallic-roughness PBR、GGX/VNDF、纹理颜色空间和 TBN normal map；再把 HDRI 环境光升级为可显式采样的 IBL 光源，并通过 light / BSDF MIS 降低 PBR + IBL 场景的方差；最后用 tile-based std::execution::par、thread-local RNG、atomic/mutex 同步完成 CPU 多线程加速，获得约 2.18x 的实测提升。\n原文：Pathtracer 梳理\n","permalink":"https://jiat-t.github.io/graphics/pathtracer-code-notes/","summary":"按路径追踪、几何与 BVH、材质接口、PBR 和 IBL/MIS 梳理个人渲染器的代码职责与工程优化。","title":"Pathtracer 梳理"},{"content":"\n一：快速排序 https://zhuanlan.zhihu.com/p/63202860\nhttps://zhuanlan.zhihu.com/p/642152546\nquickSort\n1#include \u0026lt;iostream\u0026gt; 2#include \u0026lt;vector\u0026gt; 3#include \u0026lt;algorithm\u0026gt; 4 5// 统一采取 “左闭右闭” 区间 6int partition(std::vector\u0026lt;int\u0026gt;\u0026amp; vec, int left, int right) 7{ 8 // 选取一个基准值 9 int pivot = vec[right]; 10 11 int i = left; 12 // 从左往右逐个与基准值比较 13 for (int j = left; j \u0026lt; right; j++) 14 { 15 // 如果当前值小于等于基准值 16 // 就把更小的数(vec[j])放到前面 17 // 同时将\u0026#34;小数值\u0026#34;的区域向后扩大一位 18 if (vec[j] \u0026lt;= pivot) 19 { 20 std::swap(vec[i], vec[j]); 21 i++; 22 } 23 } 24 25 // 把基准值与第一个大于它的元素换位 26 // 确保 i 左边都是小于它的元素 27 // i 右边都是大于他的元素 28 std::swap(vec[i], vec[right]); 29 // 返回基准值的下标 30 return i; 31} 32void quickSort(std::vector\u0026lt;int\u0026gt;\u0026amp; vec, int left, int right) 33{ 34 if (left \u0026lt; right) 35 { 36 int pivotIndex = partition(vec, left, right); 37 // 递归地对划分出来的左右区域进行排序 38 quickSort(vec, left, pivotIndex - 1); 39 quickSort(vec, pivotIndex + 1, right); 40 } 41} 42// 对外接口 43void quickSort(std::vector\u0026lt;int\u0026gt;\u0026amp; vec) 44{ 45 quickSort(vec, 0, vec.size() - 1); 46} 47 48int main() 49{ 50 std::vector\u0026lt;int\u0026gt; vec{ 9, 8, 7, 6, 5, 4, 3, 2, 1 }; 51 std::ranges::for_each(vec, [](const int a) {std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#34; \u0026#34;; }); 52 std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; 53 quickSort(vec); 54 std::ranges::for_each(vec, [](const int a) {std::cout \u0026lt;\u0026lt; a \u0026lt;\u0026lt; \u0026#34; \u0026#34;; }); 55} 原文：排序算法\n","permalink":"https://jiat-t.github.io/cpp/sorting-algorithms/","summary":"记录快速排序的区间划分、基准值放置和递归实现，保留对应 C++ 代码与参考链接。","title":"排序算法：快速排序"},{"content":"第 1 题：简历深挖 / 项目动机\n你简历里写了一个支持 PBR / IBL / MIS 的 CPU 路径追踪渲染器。请你介绍一下这个项目的整体架构：从场景加载、光线生成、材质采样、光照估计到最终成像，主要模块是怎么串起来的？\n总体架构 项目主要分成 Camera、Scene / Hittable、BVH、Material、PDF、Environment、Renderer 几个模块。渲染时按 tile 并行遍历像素，每个像素发射多条 camera ray，累积 radiance 后输出 PPM。 场景与加速结构 OBJ/MTL 解析生成 Triangle / Material。三角形和几何体统一进 Hittable 接口。构建 BVH，减少每条 ray 的相交测试成本。 光线与材质 Camera 生成 primary ray。命中物体后通过 Material 接口计算 BSDF、PDF、scatter 方向。材质接口拆成 Scatter / Eval / PDF，是为了让 Lambert、Metal、Dielectric、PBR 材质共用同一套积分流程。 光照估计 使用 Monte Carlo 估计渲染方程。直接光部分结合 light sampling / environment sampling / BSDF sampling。使用 MIS 的 power heuristic 平衡不同采样策略，减少只靠随机打中光源带来的高噪声问题。 结果与优化 支持 PBR、IBL、HDRI 环境光、normal map、OBJ/MTL。用 BVH、多线程 tile 渲染和 thread-local RNG 优化性能。基准场景下从 65.33s 降到 29.99s，约 2.18x 提速。 你这题改进的关键是：少讲“路径追踪一般怎么做”，多讲“我的代码里哪些模块承担什么职责”。\n第 2 题：专业深挖 / MIS\n你简历里写到实现了 HDRI 环境光重要性采样 和 light / BSDF 的 MIS 采样框架。请你解释一下：为什么只做 BSDF 采样或者只做光源采样都不够？MIS 在你的渲染器里具体解决了什么问题？\n为什么单一采样不够 Monte Carlo 积分的方差和f(x) / pdf(x)的波动有关。如果采样分布和被积函数不匹配，就会出现高方差和 firefly。BSDF 采样适合 glossy / specular lobe 明显的方向，但对小面积光源命中率低。Light sampling 能稳定采到光源方向，但对高光材质或复杂 BSDF 方向不一定匹配。 MIS 解决什么问题 MIS 不是二选一，而是把不同采样策略的贡献按权重合并。我的项目里主要结合了 light sampling、environment sampling 和 BSDF sampling。使用 power heuristic，根据当前方向在不同策略下的 PDF 计算权重，降低单一策略失配带来的噪点。 IBL 场景下为什么更需要 MIS HDRI 环境光可以看作包围场景的无限远光源。如果只靠 BSDF 随机采样，可能很难稳定采到 HDR 中高亮区域。因此对 HDRI 按亮度和球面面积做重要性采样，再和 BSDF 采样通过 MIS 合并，可以更稳定估计环境光贡献。 一句结果 所以 MIS 在我的渲染器里主要用于降低直接光、HDRI 和高光材质组合下的方差，让相同 spp 下的图像更稳定。 第 3 题：专业深挖 / PBR 材质接口\n你简历里写了 Scatter / Eval / PDF 分离式材质接口。请你解释一下为什么要这样拆？如果不拆，把所有材质逻辑都写在 ray_color() 或颜色递归函数里，会带来什么问题？\n三个接口职责 Scatter：根据材质和入射方向采样下一条 ray，返回采样方向、衰减、是否为 delta/specular 路径等。Eval：给定wo和wi，计算该方向组合下的 BSDF/BRDF 值。PDF：计算该材质在某个采样方向上的概率密度，用于 Monte Carlo 权重和 MIS。 为什么要拆 路径追踪主流程只关心“采样方向、评价 BSDF、计算 PDF、递归/迭代累积 radiance”。不同材质只需要实现自己的采样和评价逻辑，主渲染函数不需要知道 Lambert、Metal、Dielectric、PBR 的细节。这样ray_color()保持为统一积分框架，而不是堆满材质分支。 和 MIS 的关系 MIS 需要知道同一个方向在 BSDF sampling 和 light sampling 下各自的 PDF。如果没有独立的PDF和Eval，就很难把 light sampling 采到的方向再拿回材质上正确计算 BSDF 与 pdf。所以接口拆分不是单纯为了代码好看，而是为了支持多采样策略组合。 扩展性 后续增加 GGX、normal map、metallic-roughness、透明材质时，只改材质类，不破坏主渲染流程。这更接近引擎里的模块化设计思路。 你这题可以补一句关键总结：Scatter 负责“怎么采”，Eval 负责“这个方向贡献是多少”，PDF 负责“这个方向被采到的概率是多少”。\n第 4 题：专业问题 / DirectX 12 管线\n你简历里写了一个 DirectX 12 实时渲染器。请你讲一下在 DX12 中，一帧从 CPU 端提交命令到最终显示到屏幕，大致会经过哪些步骤？重点说一下 Command List、Command Queue、Back Buffer、Present 和 Fence 的作用。\n获取当前 Back Buffer 每帧先通过 Swap Chain 获取当前 back buffer index。Back Buffer 是这一帧的渲染目标之一，最终会被 Present 到屏幕。 重置命令资源 等待对应 frame resource 的 Fence，确保 GPU 不再使用这一帧的 CommandAllocator。Reset CommandAllocator 和 CommandList，开始录制本帧命令。 资源状态转换 将 Back Buffer 从PRESENT转到RENDER_TARGET。设置 RTV / DSV，清空颜色缓冲和深度缓冲。 录制绘制命令 设置 Root Signature、PSO、Descriptor Heap、Viewport、Scissor、Vertex Buffer、Index Buffer 等。调用 DrawCall，把绘制命令写入 Command List。 切回显示状态并提交 渲染结束后，将 Back Buffer 从RENDER_TARGET转回PRESENT。Close CommandList。通过 Command Queue 执行：ExecuteCommandLists()。 显示与同步 调用SwapChain-\u0026gt;Present()，把当前 Back Buffer 交给显示系统。用CommandQueue-\u0026gt;Signal(fence, value)标记 GPU 进度。下一次复用同一帧资源前，CPU 检查 Fence 是否完成。 一句话版本：\nCPU 每帧重置并录制 Command List，把 Back Buffer 从 Present 状态切到 Render Target，完成清屏、绑定管线和 DrawCall 后再切回 Present，通过 Command Queue 提交给 GPU 执行，最后 Swap Chain Present 显示；Fence 用来确认 GPU 执行进度，避免 CPU 提前复用仍在被 GPU 使用的资源。\n第 5 题：行为面试 / 问题解决\n你在做 PathTracer 或 DX12 Renderer 的时候，应该遇到过渲染结果不对、画面发黑、噪点异常、性能很慢这类问题。请选一个你印象最深的 bug 或性能问题，按“问题现象 - 排查过程 - 根因 - 解决方案 - 结果”的结构讲一下\n可以这样答：\n问题现象 接入 HDRI / IBL 后，背景能显示 HDR 环境，但画面出现大量 firefly。同样 spp 下，环境高亮区域附近噪点明显比面积光场景严重。 排查过程 先确认不是 gamma、颜色空间或贴图读取错误，因为背景显示方向和颜色基本正确。再对比面积光采样和环境光采样，发现原来的 light sampling 更适合有限面积光源，不适合直接套到无限远环境光。重点检查环境光采样的两个部分：采样分布和 PDF。 根因 HDRI 的亮度分布很不均匀，高能量集中在少数 texel。如果只靠 BSDF 采样，或者环境光 PDF 计算不正确，就会导致偶发的高贡献样本，形成 firefly。环境贴图还涉及经纬图到球面方向的转换，PDF 需要考虑sin(theta)，不能只按纹理坐标面积处理。 解决方案 在Environment模块中为 HDRI 单独实现重要性采样。按luminance * sin(theta)构建二维分布 / CDF。增加环境光自己的 PDF 接口，并在直接光估计和 MIS 权重中使用正确的环境光 PDF。用 AI 辅助检查公式推导和代码边界，但最终通过渲染结果和噪点变化验证。 结果 firefly 明显减少。相同采样数下 HDRI 高亮区域的收敛更稳定。也让后续 light sampling、environment sampling、BSDF sampling 能通过 MIS 放到统一框架里。 这题你可以保留“AI 辅助验证”，但主表达要从“我问了 AI”改成“我定位到 PDF 和采样分布不匹配，然后用工具辅助校验”。\n第 6 题：专业问题 / HDRI 环境光采样\n你刚才提到了给 Environment 增加自己的 PDF 接口。那我继续追问：如果使用经纬度 HDR 贴图做环境光采样，为什么构建采样分布时通常要乘上 sin(theta)？如果不乘，会造成什么问题？\n经纬图不是等面积映射 HDRI 经纬图中每个 texel 在 2D 贴图上面积一样。但映射到球面后，不同纬度对应的立体角不同。球面面积微元是：domega = sin(theta) dtheta dphi。 为什么乘sin(theta) 赤道附近sin(theta)接近 1，同样大小 texel 对应更大的球面面积。极点附近sin(theta)接近 0，同样大小 texel 对应更小的球面面积。所以构建环境光重要性采样分布时，通常用luminance(texel) * sin(theta)，让采样概率同时反映亮度和球面面积。 不乘的后果 会把极点附近的小立体角 texel 采得过多。PDF 和真实积分测度不匹配，可能导致噪声变大、能量分布偏差，严重时环境光亮度估计不稳定。在 MIS 中还会影响权重计算，因为环境采样 PDF 不准确。 一句话版：\n因为经纬图到球面的映射不是等面积的，球面面积微元包含 sin(theta)。构建环境光采样分布时乘上它，是为了让 texel 的采样概率和它实际覆盖的立体角匹配，否则极区会被过度采样，PDF 也会和真实环境光积分不一致。\n第 7 题：简历深挖 / 性能优化\n你简历里写 PathTracer 通过 BVH、多线程 tile 渲染和 thread-local RNG，把渲染时间降低了 2.18 倍。请你具体讲一下这个优化是怎么做的？为什么 tile 并行可以避免数据竞争？有没有哪些部分仍然可能成为瓶颈？\n原始问题 最初是单线程扫描线渲染，每个像素按顺序计算。路径追踪每个像素要发射多个 sample，每个 sample 又可能递归多次 bounce，CPU 多核利用率不足。 tile 并行 我把图像划分成固定大小的 tile，例如每个 tile 包含一块连续像素区域。多线程并行处理不同 tile。每个 tile 只写自己负责的像素范围，最终 framebuffer 的索引由(x, y)唯一确定，所以不同线程不会写同一个像素，避免数据竞争。相比“每个像素一个任务”，tile 粒度可以减少任务调度开销，也有更好的 cache locality。 thread-local RNG 路径追踪采样需要大量随机数。如果多个线程共享 RNG，会有锁竞争或数据竞争。所以我使用 thread-local RNG，让每个线程有自己的随机数状态，减少同步开销。 BVH 优化 未加 BVH 时，每条 ray 都要遍历大量三角形。BVH 用层次包围盒先剔除不可能命中的区域。ray 先和 AABB 求交，只有命中包围盒才继续访问子节点或叶子三角形，从而显著减少三角形求交次数。 量化结果 在 800x450 / 400 spp / 20 depth 的基准下，串行渲染约 65.33s，并行后约 29.99s，整体提速约 2.18x。 剩余瓶颈 高 spp 仍然是主要成本。复杂场景下 BVH traversal、材质 BSDF 计算、环境光采样和内存访问仍然占比较高。不同 tile 的复杂度不同，也可能造成线程负载不均衡。 一句话总结可以这样说：\ntile 并行的关键不是让线程随便抢像素，而是把 framebuffer 划分成互不重叠的写入区域；再配合 thread-local RNG 避免随机数状态竞争，BVH 则减少每条 ray 的求交成本。\n第 8 题：专业问题 / DirectX 12 IBL\n你在 DX12 Renderer 里写了 split-sum IBL，并预计算了 irradiance map、prefiltered environment map 和 BRDF LUT。请你解释一下这三个贴图分别解决什么问题？运行时渲染一个 PBR 物体时，它们是怎么被用到的？\n为什么需要 split-sum IBL 离线路径追踪可以对环境光做大量采样。实时渲染不能每个像素每帧采很多条环境光方向。所以把环境光积分拆成可以预计算的部分和运行时快速查表的部分。 irradiance map 解决 diffuse IBL。它对环境贴图按每个法线方向做半球卷积。运行时用法线N采样 irradiance map，再乘以 diffuse albedo，近似漫反射环境光。 prefiltered environment map 解决 specular IBL 中环境反射采样成本高的问题。按不同 roughness 对 HDR 环境贴图做 GGX 重要性采样预滤波，并存到不同 mip level。运行时用反射方向R采样，并根据 roughness 选择 mip，roughness 越大结果越模糊。 BRDF LUT 解决 specular BRDF 积分中和视角、粗糙度相关的部分。输入是NdotV和roughness。输出通常是 scale / bias 两个值，用来和F0组合。 运行时组合 diffuse：irradiance(N) * albedospecular：prefilteredEnv(R, roughness) * (F0 * brdf.x + brdf.y)最后再加上直接光、阴影、tone mapping 等。 一句话总结：\nirradiance map 负责漫反射环境光，prefiltered environment map 负责不同粗糙度下的环境反射，BRDF LUT 负责视角和粗糙度相关的 BRDF 积分项；三者合起来让 PBR 材质在实时渲染中用少量纹理采样近似 IBL 积分。\n这题你的方向是对的，补上 N、R、roughness mip、NdotV、F0 这些关键词，会更像真正写过 shader。\n第 9 题：行为面试 / AI 工具使用\nJD 里提到熟练使用 CodeBuddy、Codex、Claude Code 等 AI 工具，并且你简历里也写了使用 AI 工具辅助开发。请你讲一个具体例子：你在项目里是怎么使用 AI 工具提升开发效率的？你如何判断 AI 给出的方案是正确的，而不是直接照抄？\n场景 在 PathTracer 中集成 PBR 流程时，我一开始掌握了 Cook-Torrance、GGX、Fresnel 等公式，但不确定怎么接入现有渲染器架构。 AI 的作用 我先用 AI 帮我拆分实现路径，比如材质接口、BSDF eval、PDF、采样、贴图解析、测试场景。然后让 AI 结合源码指出现有架构适合在哪些模块接入，而不是直接生成一整套代码。 自己的判断 我会先复习公式和参考开源实现，确认 AI 方案是否符合渲染方程和现有代码结构。对关键部分，比如 GGX VNDF、normal map、IBL、MIS，我会重点检查 PDF、坐标空间和能量项是否一致。 验证方式 每完成一个小模块，就用固定测试场景验证，例如不同 roughness、metallic、normal map、HDRI 光照。再让 AI 做 code review，检查边界条件、变量含义和潜在 bug。最后通过渲染结果和性能数据确认效果，比如多线程优化后的 2.18x 提速。 总结 AI 主要提升了方案拆解、资料对比和代码审查效率，但最终是否采用，还是由我基于公式、源码结构、测试图和 benchmark 判断。 一句话版本：\n我不会直接照抄 AI 代码，而是把 AI 当作方案拆解和 review 工具；关键图形逻辑会用公式推导、源码结构、固定测试场景和渲染结果来交叉验证。\n原文：项目细节\n","permalink":"https://jiat-t.github.io/graphics/pathtracer-project-details/","summary":"整理个人 CPU 路径追踪器的模块分工、PBR 与 IBL、MIS 采样及 BVH 和多线程性能优化。","title":"CPU 路径追踪器项目细节"},{"content":"【Reference】：https://zhuanlan.zhihu.com/p/104687855\nhttps://zhuanlan.zhihu.com/p/369710758\nhttps://zhuanlan.zhihu.com/p/483674565\n一：Shadow Map 是什么 之前已经写过了，所以这里不再赘述\n详情请见：https://jiat-t.github.io/posts/shadow-techniques/\n二：锯齿与软阴影 1.PCF（Percentage-Closer Filtering，百分比渐近滤波） Shadow Map 在进行深度比较时，所得结果非零即一，这无疑会导致非常严重的锯齿化问题\nPCF 采用的抗锯齿方法是：对于目标像素，我们并不仅仅只对这一个像素的深度进行比较，而是在它周围 3x3 或是 5x5 等范围内的每一个像素都进行深度比较；最后将所有比较的值相加求平均，可以得到一个【0，1】范围内的阴影系数（0 表示完全遮挡，1 表示无遮挡）\n同时，对周围点的采样方式也可以是多样的，比较常见的就是使用泊松分布来进行选择周围像素\n2.PCSS（Percentage-Closer Soft Shadows, 百分比渐近软阴影） 首先明确一个物理现象：\n遮挡物离接收平面越近，阴影越硬\n遮挡物离接收平面越远，阴影越软\n而 PCF 对阴影的软化是相对于整体而言的，无法做到随距离而变化，PCSS 则能够通过距离控制半影宽度，从而实现软阴影的效果\n具体公式（相似三角形）：\n计算出 W_Penumbra 之后，只需将其乘上一个缩放因子，转化为 Shadow Map 中的采样半径，即可对 PCF 的采样范围进行控制（距离越近，PCF 采样范围越小，阴影就越硬；反之，PCF 采样范围越大，阴影更软）\n3.VSM（Variance Shadow Maps, 方差阴影贴图） 首先说说“切比雪夫不等式”：\n$ P(x \u003e t) ≤ \\frac{\\sigma^2}{\\sigma^2 + (t - \\mu)^2} $\n其中$ \\mu $是平均值，$ \\sigma^2 $是方差；这个不等式的含义是： 在一个均值为$ \\mu $，方差为$ \\sigma^2 $的分布中，任取一个随机变量 x，其值大于 t 的概率小于某一个上界\n回到 PCF 中，如果直接使用 PCF 去做软阴影，需要使用一个 for 循环对 shadow map 进行多次采样与深度比较，这会极大地拖慢运行速度；那么，有没有一种方法能够避免多次的查询，在渲染物体时只进行一次采样呢？\n最理想的情况是：直接模糊掉 shadow map，然后每次都只采样一次，但这很显然是错的，因为最终的深度比较结果仍然非零即一。既然如此，不妨转变一下思路：既然最后要求得的都是一个 0~1 范围内的阴影系数，那我们不妨将这个系数等效为一种“概率”—— 即当前片元没有被遮挡（光线能够穿透）的概率\n如果将 shadow map 上的深度视作随机变量，那么计算阴影系数，就变成了求“遮挡物深度大于当前片元深度”的概率 P(x \u0026gt; t)。此时，切比雪夫不等式就派上用场了\n因为基础的 shadow map 只存有 z 值，而切比雪夫不等式需要使用的是方差与均值，因此需要对其进行额外的存储，具体操作步骤如下：\n增加存储内容：除了存储 Z 值本身外，还需要额外对 $ Z^2 $ 进行存储（可以使用两张 shadow map，或者是将其存储在一张 shadow map 的不同通道）\n模糊操作：这是 VSM 最大的优势。我们可以像处理普通图片一样，直接对这张双通道 Shadow Map 进行高斯模糊或硬件 Mipmap 过滤。模糊后，我们单次采样得到的值，就直接是该区域内深度的期望 E(x) 和深度平方的期望 E(x^2)\n计算方差与概率：根据概率论公式，方差可以通过 $ \\sigma^2 = E(x^2) - (E(x))^2 $ 算出。拿到方差$ \\sigma^2 $和均值 $ \\mu $后，直接代入切比雪夫不等式，就能估算出一个上限概率\n优点：性能极高，只需要进行一次采样操作即可得到效果不错的软阴影\n缺点：漏光现象——当两个遮挡物的深度差异非常大时，方差会变得特别大。根据上面的公式，方差一旦变大，计算出来的光照概率 P 就不会是 0，而是一个比较大的值。这会导致原本应该处于死角、完全是黑影的地方，诡异地透出光来\n4.VSSM（Variance Soft Shadow Maps，方差软阴影） 如果说 VSM 是 PCF 的升级版，那么 VSSM 则是 PCSS 的升级版\n在 PCSS 中，开销最大的一步就是“求得遮挡物的平均深度 $ Z_{occ} $”，因为需要通过 for 循环去暴力求解，而这一步也正是 VSSM 对其进行改进的点，即：只使用数学公式，一步直接算出平均深度\n公式如下：$ P_{unocc} \\cdot Z_{unocc} + (1 - P_{unocc}) \\cdot Z_{occ} = Z_{Avg} $\n其中：\n未被遮挡像素的比例 $ \\frac{N_1}{N} $，其实就是当前片元处于光照中的概率，我们记为 $ P_{unocc} $\n被遮挡像素的比例 $ \\frac{N_2}{N} $，就是处于阴影中的概率，记为 $ 1 - P_{unocc} $\n移项可得：$ Z_{occ} = \\frac{Z_{Avg} - P_{unocc} \\cdot Z_{unocc}}{1 - P_{unocc}} $\n观察右边的式子，其中：\n$ Z_{Avg} $（整个区域的平均深度）： 可以直接通过对生成好的 Shadow Map 进行 Mipmap 查询，或者使用 SAT（Summed Area Table）瞬间得到\n$ P_{unocc} $（未被遮挡的概率）：就是 VSM 里用切比雪夫不等式算出来的那个光照概率 $ P $ ，通过方差 $ \\sigma^2 $ 可以直接得出\n$ Z_{unocc} $（未遮挡部分的平均深度）： 既然它没被遮挡，说明光线直接打到了接收面上。因此，可以采用了一个非常巧妙的近似假设——直接把当前正在着色的片元深度 $ t $作为 $ Z_{unocc} $ 的值\n最终得到遮挡物的平均深度$ Z_{occ} \\approx \\frac{Z_{Avg} - P \\cdot t}{1 - P} $\n5.MSM（Moment Shadow Mapping，矩阴影贴图） MSM 解决的是 VSM 的漏光现象，可以理解为“VSM 是 MSM 的青春版”\n什么是”矩“？\n在统计学和数学中，“矩”是用来描述概率分布形状的数值：\n一阶矩：均值 E(z)，也就是 VSM 里的$ \\mu $\n二阶矩：平方的均值 E(z^2)，VSM 利用它算出了方差$ \\sigma^2 $\nVSM 的本质，其实就是“二阶矩阴影贴图”。 它利用两个矩，通过切比雪夫不等式去猜（估算）深度的分布。但当遮挡物深度跨度极大时，仅靠两个矩提供的信息太少了，估算出的概率曲线会过于平缓，导致本该是死黑的阴影被错误地算出了大于 0 的光照概率——这就是漏光的数学根源\nMSM 的标准做法是存储四个矩（4-Moments）。 在生成 Shadow Map 时，它不仅存 z 和 z^2，还要存 z^3 和 z^4。 之后依然可以像 VSM 那样，对这张包含了 4 个通道的贴图进行暴力的高斯模糊或硬件滤波\n可以看到，四阶矩已经可以很好的近似 PCF 了：\n至于之后 CDF 的复原，涉及到非常复杂的数学推导，这里不作展开\n三：精度与分辨率 1.PSM (Perspective Shadow Maps, 透视阴影贴图) 对于一张 1024 x 1024 的 shadow map，我们常常会遇到这样的情况：明明远处的阴影看上去还行，但是近处的阴影却是一团马赛克。造成这种现象的原因是“投影面积不匹配”，相较于远处的阴影，近处的阴影明明在屏幕上占据更大的区域（换句话说是，占据的像素更多），但 shadow map 还是使用与远处阴影一样的纹素数量来表示它（比如 2 x 2 个纹素）\n解决的方法也很简单，既然分配给近处的纹素太少，那为什么不增加近处的采样数量呢？\n在标准的 shadow map 流程中，我们是在世界空间下，从光源的视角去拍摄场景并生成深度图；此时由于正交投影，拍摄出来的物体是不存在“近大远小”的关系的，因此只需要将近处的物体放大，将远处的物体缩小，就可以达到区分阴影精度的效果；所以，唯一需要做的就是将“正交投影”更改为“透视投影”\n操作步骤：\n空间变换：PSM 放弃在世界空间中直接生成 shadow map，而是使用观察矩阵与透视矩阵将坐标变换到 NDC 空间，在这个空间中，物体完美符合人眼中的景象——近大远小\n光源采样：计算出光源在这个 NDC 空间中的位置与方向，并生成这个空间下的 shadow map\n结果：近处的物体在 shadow map 上占据了更多纹素，远处的物体占据更少的纹素\n这里用一段 Gemini 的例子来形象地理解一下：\n假设你正在玩一个第一人称射击游戏，阳光从你头顶正上方垂直照下来。 在你脚边有一只小猫（离相机极近），在远处的山头有一只大象（离相机极远）。\n在世界空间下（标准正交阴影贴图）： 阳光垂直往下拍。因为是正交投影，小猫在 Shadow Map 上就占猫那么大的像素，大象就占大象那么大的像素。但是，因为小猫离玩家太近了，走上前去一看，猫身上的影子全是马赛克。 在 PSM 的流程下： 第一步，我们不让阳光拍。我们先让相机的透视矩阵把这个世界扭曲一下。 在相机的透视镜头里，由于“近大远小”的视觉原理，脚边的小猫因为离相机太近了，在屏幕上变得巨大无比；而远处的山头和大象因为离相机太远了，在屏幕上缩得只有指甲盖那么大。 第二步，重点来了！阳光在这个时候砸下来。 此时，阳光看到的不再是原本的世界，而是一个被相机镜头扭曲后的世界。在这个扭曲的世界里，小猫变成了“巨猫”，大象变成了“微雕大象”。 当阳光（依然用正交投影）去拍摄这个扭曲的世界生成 Shadow Map 时，这张贴图上大半张纸都被这只“巨猫”给占满了，而远处的“微雕大象”只分到了几个像素。\n现在 PSM 由于巨大包围盒的计算难题、阴影质量随相机旋转剧烈波动等问题，以及显存与算力的提升，已经完全被淘汰了\n2.CSM (Cascaded Shadow Maps, 级联阴影贴图) CSM 的核心思想其实于 PSM 差不多：增加近处阴影的分辨率，降低远处阴影的分辨率，而 CSM 也是许多引擎（如：UE、Unity\u0026hellip;）在处理太阳光之类覆盖大场景的方向光时的首选\n操作步骤：\n首先沿着玩家视角，根据深度值将视锥体划分为不同层级（比如 3 层、4 层），这里以 3 层为例： Cascade 0 (近景)： 离相机极近，虽然深度范围很窄（比如 0~10米），但我们给它分配一张完整的 1024x1024 贴图。这里的像素密度极高，能看清睫毛的阴影 Cascade 1 (中景)： 比如 10~50 米，也分配一张 1024x1024 的贴图 Cascade 2 (远景)： 比如 50~200 米，同样是 1024x1024。由于覆盖的物理面积太大了，所以这层阴影会很糊，但是又因为离相机很远，因此玩家根本注意不到 不过这种直接写死层级范围的方法可能会导致 shadow map 内存的浪费（e.g. 近处的物体太少），所以实际使用时一般都会结合指数或其他手段进行划分 计算包围盒与正交矩阵： 对于切出来的每一块子视锥体： 算出这个子视锥体在世界空间下的 8 个顶点坐标 将这 8 个点转换到光源的观察空间 计算这 8 个点的 AABB，从而推导出当前这一层的正交投影矩阵 渲染 Shadow Map： 光源依次套用上面算出的 3 个正交矩阵，把场景渲染 3 遍（可以配合视锥体剔除，没在当前 Cascade 里的物体就不画了） 像素着色器采样： 进入 FS，得到每个像素点的深度之后，先用几个 if/else 判断其位于哪一层级的视锥体/ Cascade，再通过索引（如果使用数组存储 Shadow Map 的话）和光源矩阵，将 Shadow Map 中的深度值提取出来进行比较 可能遇到的问题：\n层级混合：由于相接的两张 Shadow Map 精度不同，清晰程度自然也就不一样，这种差异在两个层级的接缝处会显得尤其强烈——这一边是锐利的阴影，而另一边却直接糊成了一团马赛克\n解决方法是使用 Cascade Blending（级联混合） —— 在两个层级之间定义一个缓冲带，Shader 会在缓冲带区域进行两次采样，并根据距离对两张 Shadow Map 进行混合。这种做法虽然可以实现有效的过渡，但代价也很明显：位于缓冲带的点，其 Shadow Map 的采样开销相对于缓冲区外的点的会翻倍\n阴影抖动：当视角进行转动时，子视锥体的包围盒也会发生微小的变化，这意味着光源的正交矩阵在视角移动过程中的每一帧都在发生变化，反映在图像上就是：阴影的位置总是在变化，这种现象在硬阴影中尤为明显\n解决方法是使用 Stabilize Cascades（稳定级联） —— 简单来说就是将原本的 AABB 替换为一个 “恒定包围球”，以这个包围球为内切球的立方体就是新的 AABB；至于包围球怎么寻找：先找出视锥体切片的中心点，并以中心点到最远顶点的距离（对角线）作为半径，画一个球，这个球就是包围球\n","permalink":"https://jiat-t.github.io/posts/shadow-maps/","summary":"整理 Shadow Map 的软阴影、概率阴影、矩阴影以及 PSM、CSM 等精度与分辨率优化方法。","title":"Shadow Maps"},{"content":"【Reference】：https://zhuanlan.zhihu.com/p/27572129\n一：平面阴影（Planar Shadow） 通过联立投影点在光线与平面的两个方程，可以求解出投影点的位置。\n直线方程：\n1p = l + t * (v - l) 平面方程：\n1n * p + d = 0 其中，p 是投影点，l 是光源位置，v 是投影物体表面任意一点，n 是法线，d 是平面与原点的距离，t 是比例系数。\n但是实际场景中并不会每次都联立两个方程求解，而是将整个阴影投射计算写成一个平面阴影矩阵。之后使用的时候，只需要将投影点乘上对应的平面阴影矩阵，即可得到投影在对应平面上的点的位置。\n具体推导请见：Shadow Map Derivation\n注意：平面阴影技术并不是将被投影平面上对应的点变黑，而是额外生成了一个投影几何体。\n实现过程中可能出现的问题：\n1. 半透明叠加 产生原因：在实际渲染中，为了模拟全局光照的贡献，阴影并不会被设置为全黑，而是带有一定透明度的灰色。此时当三角面被投影至平面时，有可能会产生重叠，导致同一处阴影由于 alpha 值叠加变为黑色。\n解决方法是模板测试：先绘制投影平面，并开启模板测试。模板缓冲可以设置为 1，测试函数设为 EQUAL。当模板测试通过一次后，模板值加 1，保证每个点只会执行一次阴影绘制。\n2. Z-Fighting 本质：深度缓冲精度不足导致的冲突。当两个面距离过近，甚至完全重合时，由于浮点数的精度误差，Z-Test 无法分辨哪一个平面在上，于是就出现了两个平面交替渲染的错误，表现在屏幕上就是“闪烁”“撕裂”的杂乱纹理。并且由于 Z 值的非线性，越远处的物体之间的深度值差距越小，因此也就更加容易出现 Z-Fighting。\n两种解决方法：\n先绘制平面，然后关闭深度测试与深度写入，进行平面阴影的绘制，最后再开启深度测试与写入，执行被投影模型的绘制。 添加一个小小的 bias：在计算中稍微加大或减少一点 d 的值，具体取决于法线方向，让阴影与平面之间产生一点距离，也就是略高于平面。 二：阴影体（Shadow Volume） 对于每一个受光照的物体，其自身会含有一个两端开口的棱台状的阴影体。位于阴影体中的点，就是需要绘制阴影的地方。\n此时问题变成了：如何知道一个点是否位于阴影体之中？\n解决方法是模板测试：对于从摄影机向屏幕上任意一点发出的视线，如果与阴影体正面相交，说明进入了阴影体，模板值加一；如果与阴影体背面相交，说明离开了阴影体，模板值减一；如果最终模板值不为零，也就是大于零，说明射线在此处受到了阻挡，只能进去但是出不来，因此可以判断此点一定位于阴影中。\n具体实现：\n首先关闭 Stencil Write、Stencil Test，渲染一遍不带阴影的场景，只写入深度与颜色。 关闭 Z Write，Z Test 保持为 Less Equal，将 Stencil Write 设置为 1，Stencil Test 设置为 Always，绘制阴影体正面。 Stencil Write 设置为 -1，绘制阴影体背面。 关闭 Stencil Write，Stencil Test 和 Z Test 设置为 Equal，对 stencil 为 0 的点进行阴影绘制。 以上操作都是基于“摄影机在阴影体外部”的假设进行的。如果摄影机一开始就在阴影体中，会导致明暗关系完全反转，此时需要使用 Z-Fail 来进行阴影绘制：\n逻辑：之前的操作是从摄影机往外看，而 Z-Fail 的操作完全相反，它是从无限远处往摄影机回看。 操作：未通过深度测试时，如果遇到背面，Stencil++；如果遇到正面，Stencil\u0026ndash;。 代价：需要保证阴影体完全闭合。之前的阴影体没有上盖和下盖，因此这会产生额外的几何处理开销。 缺点：大量物体重叠时会导致 Overdraw；不支持软阴影；Caster 必须是完全闭合的几何体；CPU / 几何管线开销过大。\n三：阴影映射（Shadow Mapping） Shadow Mapping 是目前应用最多的阴影算法，因为它简单直观，并且与光栅化流程极度吻合。\n核心思想：一个点能否被照亮，取决于从光源的视角看过去，这个点是否可见。\n实现步骤：\n光源视角：将摄影机放置在光源的位置上。平行光用正交投影，点光 / 聚光用透视投影，对场景进行绘制，生成一张深度图。 摄影机视角：再次绘制场景，得到当前投影空间的深度值 d，并使用光源投影矩阵的逆矩阵得到对应点在光源投影空间中的 uv 坐标。 深度值比较：利用之前得到的光源投影空间坐标，在光源视角深度图中查找对应深度值 z。如果 d \u0026lt; z，说明当前点摄影机看得到，但是光源看不到，也就说明当前点被遮挡，处于阴影中。 可能出现的问题：\nShadow Acne / 自阴影：由于 Shadow Map 的精度问题，在倾斜的表面上进行深度比较时，可能会有一部分片段的深度错误地大于记录的深度，出现“自己遮挡自己”的现象。解决方法是深度偏移：为采样得到的深度添加一个小小的偏移，也就是 d + bias。 彼得潘现象（Peter-Panning）：当 bias 的值过大时，物体根部会与阴影发生断裂，这就是所谓的 Peter-Panning。一种解决方法是基于法线的偏移：根据法线与光线的点积，动态调整 bias 的大小，光线越平行于表面，与法线的点积越小，bias 越大；还有一种解决方案是正面剔除：在 Pass 1 中生成深度图时开启 Front Cull，强制渲染背面的深度；而在 Pass 2 中比较深度值时，还是使用正面的深度，前提是物体必须是闭合的、有厚度的。 四：SDF 阴影（Signed Distance Field Shadows） SDF（有向距离场）：一种用空间体积数据表示几何体的方法。在这个场中，空间里的每一个点都记录着一个浮点数，代表该点与最近的物体表面的最短距离。\n值为正：该点在几何体外部。 值为 0：该点正好贴在几何体表面上。 值为负：该点在几何体内部。 在 SDF 中计算阴影使用到的是 Ray Marching（射线步进），更具体的说法是 Sphere Tracing（球体追踪）。\n具体实现：\n从 Shading Point 出发，向光源方向发射一条阴影射线，开始步进。 首先在出发点对 3D 纹理进行一次查询，得到一个距离值 D。D 的含义是：在以当前点为圆心、D 为半径的圆内，不存在任何遮挡物。因此，可以大胆地沿着射线向前步进 D 的距离。 重复上述检测，直到发生以下情况之一： 抵达光源 / 超出最大距离：说明光线一路畅通无阻，该点不在阴影中。 查询到的 SDF 距离极小，小于设定的 epsilon 阈值：说明光线撞到了表面，该点在阴影中。 SDF 阴影不仅能够避免复杂的光线与物体求交运算，还可以在单次步进中就得到软阴影的效果。我们可以利用“光线与障碍物的最小靠近程度”来估算半影区（Penumbra）的可见度，其估算公式为：\n1res = min(res, k * D / t) D：当前步进点查询到的最近表面距离，也就是 SDF 值。 t：光线从起点出发已经走过的总距离。 k：控制光源大小，也就是阴影软硬程度的常数参数。 缺点：\n对动态物体支持极差：因为 SDF 通常是预计算得到的。 显存与精度问题：高精度的 3D 有向距离场非常吃显存，因此通常不会使用精度太高的 SDF。 五：光线追踪阴影（Ray-Traced Shadows） 光线追踪阴影是所有阴影技术中最“物理正确”的一个，毕竟要用于离线渲染。\n光线追踪阴影的本质是：可见性测试。\n对于直接光照来说，从像素点向光源发出一条射线，如果能够直接击中光源，说明此点未被任何物体遮挡；反之，如果打中了某个物体，说明这一点并不能被光源直射到。\n为什么没有考虑多次弹射后打到光源的情况？\n在实际应用中，渲染方程往往被拆成 Emitted Light + Direct Light + Indirect Light 三项来计算。光线弹射的情况属于 GI 的部分，也就是第三项 Indirect Light，而 Ray-Traced Shadows 属于第二项，也就是直接光照部分 Direct Light。这样的拆分在工程上是合理的，因为可以节约算力。\n光线追踪阴影同样完美支持软阴影：\n我们从着色点向面光源的表面随机发射多条射线，也就是蒙特卡洛积分。 统计这些射线的“存活率”。 如果 100% 的射线被挡住，这里就是本影（Umbra），绝对黑暗。 如果只有 40% 的射线被挡住，这里就是半影（Penumbra），呈现柔和的灰度。 由于遵循严格的几何关系，光追软阴影会自动呈现出“距离遮挡物越近，阴影越锐利；距离越远，阴影越模糊”的渐变。\n六：光照贴图（Light Map） 以上说到的五种计算阴影的方法都是在运行期进行的，是动态的阴影；但是对于一些静态的物体（e.g. 房子、山体\u0026hellip;.），他们的阴影完全不会变化，为什么不提前进行计算呢？\nLight Map 方法指的就是，在游戏发行前，通过离线的路径追踪算法，提前渲染出极高质量的阴影与间接光，将其存成一张图片，在游戏运行时，只需要进行一次极低成本的纹理采样即可\n缺点也很明显：场景中的物体一定不能产生任何变化，否则就会出现“只有阴影，没有投影体”的情况\n","permalink":"https://jiat-t.github.io/posts/shadow-techniques/","summary":"整理平面阴影、阴影体、Shadow Mapping、SDF 阴影与光线追踪阴影的基本原理、实现步骤和常见问题。","title":"阴影技术"},{"content":"【Reference】：\nSSAA、MSAA：https://www.zhihu.com/question/20236638/answer/44821615\nFXAA：https://zhuanlan.zhihu.com/p/431384101\nSMAA：https://zhuanlan.zhihu.com/p/342211163\nTAA：https://zhuanlan.zhihu.com/p/425233743\n锯齿产生的原因：本质是采样频率不足以还原物体的高频细节，更通俗一点的说法是”试图用离散的像素去表现连续的物体“\n一：空间与硬件级的抗锯齿 1. 超采样抗锯齿（Super Sample Anti-Aliasing， SSAA） SSAA 的做法非常暴力：先以原分辨率的 N 倍分辨率对场景进行渲染，然后再通过降采样得到原分辨率的图像（e.g. 以 4x SSAA 为例，欲渲染一张 1024 x 1024 的图像，先以 4096 x 4096 为分辨率渲染一张图像，再将降采样至1024 x 1024）。这在数学上是完美的抗锯齿，但是光栅化与着色的计算量都会增加 N 倍，所以基本上不会使用这个方法。\n2. 多重采样抗锯齿（Multi Sample Anti-Aliasing， MSAA） MSAA 可以说是 SSAA 的改进版\n在光栅化阶段，当判断三角形是否被像素所覆盖时会采样多个覆盖样本（如下图）\n之后执行像素着色器的时候，还是只会在像素中央计算一次，但是要乘以“覆盖样本与总样本数的比值”（这里为 0.5）\n相对于 SSAA，MSAA 的计算量更小，但是 MSAA 也有缺点：与延迟渲染不兼容\n因为延迟渲染是分为 G-Buffer（几何缓冲区） 和 Lighting 两个 Pass，以 4x MSAA 为例，这意味着如果强行在 Deferred Shading 中使用 MSAA，每个像素都需要额外存储 4 个亚像素样本的数据，这样不仅会使得显存的占用翻 4 倍，还会导致显存带宽的崩溃（在几何阶段写入 4 倍的数据，在光照阶段读出 4 倍的数据）\n二：后处理抗锯齿 1. 快速近似抗锯齿（Fast Approximate Anti-Aliasing， FXAA） FXAA 将抗锯齿问题降维成 2D 图像的边缘检测与定向模糊问题，使其与渲染管线完全解耦\n操作步骤：\n1. 提取像素亮度值\n$Luma = R \\times 0.299 + G \\times 0.587 + B \\times 0.114$\n实际场景中可以直接使用 G 通道进行优化，因为绿色对亮度的贡献最大。\n2. 对比度检测（边缘检测）\n额外采样上、下、左、右四个方向的亮度值，找出最大、最小亮度，并计算的差值（局部对比度）：$ L_{max} - L_{min} $。如果该差值大于等于设定的阈值，就可以认为此像素位于边缘处，需要进行抗锯齿；反之则认为不处于边缘，不参与后续的模糊过程，这里也是 FXAA 高性能的原因（直接舍弃）。\n3. 确定边缘朝向\n对于通过对比度检测的像素，需要再额外采样左上、左下、右上、右下四个角的亮度，然后分别计算水平方向与竖直方向上的变化幅度：\n1// 计算水平方向的亮度变化幅度 2float Vertical = abs(N + S - 2 * M) * 2 + abs(NE + SE - 2 * E) + abs(NW + SW - 2 * W); 3// 计算垂直方向亮度变化幅度 4float Horizontal = abs(E + W - 2 * M) * 2 + abs(NE + NW - 2 * N) + abs(SE + SW - 2 * S); 5// 判断边界的方向 6bool IsHorizontal = Vertical \u0026gt; Horizontal; 7// 根据边界方向，先算出后面搜索时的步长 8float2 PixelStep = IsHorizontal ? float2(0, _MainTex_TexelSize.y) : float2(_MainTex_TexelSize.x, 0); 再确定正负方向：\n1float Positive = abs((IsHorizontal ? N : E) - M); 2float Negative = abs((IsHorizontal ? S : W) - M); 3if(Positive \u0026lt; Negative) PixelStep = -PixelStep; 4. 沿边缘搜索\n自定义“最大迭代次数”，在边缘处通过循环进行搜索，每次步进时记录当前像素对比度，直到对比度发生显著变化，此时可以认为找到了锯齿的“端点”。\n通过计算当前像素到两个端点的距离，FXAA 能够推断出当前像素在边缘的位置。\n5. 混合系数计算\n根据下图权重计算像素参与抗锯齿混合的强度：\n1float Filter = 2 * (N + E + S + W) + NE + NW + SE + SW; 2Filter = Filter / 12; 3// 计算出基于亮度的混合系数 4Filter = abs(Filter - M); 5Filter = saturate(Filter / Contrast); 6// 使输出结果更加平滑 7float PixelBlend = smoothstep(0, 1, Filter); 8PixelBlend = PixelBlend * PixelBlend; 6. 混合\n根据步骤 4 计算出的比例，可以得到一个偏移量：如果像素靠近边缘中心，偏移量就大；靠近端点，偏移量就小。最后将原始的 UV 坐标垂直于边缘方向移动这个偏移量，进行一次双线性插值（Bilinear）采样，得到混合后的最终颜色。\n2. 亚像素形态学抗锯齿（Subpixel Morphological Anti-Aliasing， SMAA） FXAA 存在一些局限性：仅仅通过亮度差异暴力地寻找边缘，存在边缘检测不准的问题，并且可能导致图像模糊\nSMAA 通过引入大量预计算内容，对不同边缘类型进行匹配，基本解决了这些问题\n核心——模式匹配算法：\nSMAA 会提前计算不同的边缘形态（如 Z 型，U 型，L 型\u0026hellip;），并将对应的偏移权重（对应 FXAA 第 6 步）存储在贴图中。之后，在进行渲染时，shader 只需要像查表一样对不同的边缘类型进行判断，从而找到最精确的亚像素偏移\nSMAA 通常需要使用三个 Pass：\nPass 1：边缘检测\n与 FXAA 类似，通过亮度对边缘进行判断，但是 SMAA 还会额外采样深度或模板信息进行判断 Pass 2：权重计算\n将边缘检测结果与模式贴图进行匹配，计算出每个边缘像素需要向哪个方向、移动多少距离 Pass 3：混合\n通过 Pass 2 得到的权重信息，对当前像素与邻近像素进行混合 三：时间抗锯齿 TAA（Temporal Anti-Aliasing） 核心思想：既然增加当前帧的采样太昂贵，就把采样的成本分摊到过去的多帧中\n操作步骤：\n1. 亚像素投影抖动\n在执行完顶点着色器，进行透视除法之前，使用某种低差异序列（通常是 Halton 序列）对透视投影矩阵的【2，0】和【2，1】进行替换。\n目的：即使物体完全静止，每一帧的像素边界也都存在差异，不仅保证 TAA 在物体未运动时也能生效，同时也使得每一帧的边缘基本都不一样，从而实现混合后的抗锯齿效果。\n2. 拿到运动矢量\n因为需要使用过去帧，所以必须知道当前像素在上一帧的哪个位置。也就是说，在渲染 G-Buffer 时，要同时输出一张 Velocity 贴图，并且还需要传入上一帧的世界、投影矩阵，使用当前的世界坐标反推出像素在屏幕空间中的位移。\n3. 历史重投影\n进入 TAA Pass 之后，需要先移除像素采样时添加的抖动值，然后减去 Motion Vector 得到上一帧的历史 uv 坐标，最后使用双线性插值计算颜色：\n1// 减去抖动坐标值，得到当前实际的像素中心UV值 2uv -= _Jitter; 3// 减去Motion值，算出上帧的投影坐标 4float2 uvLast = uv - motionVectorBuffer.Sample(point, uv); 5//使用双线性模式采样 6float3 historyColor = historyBuffer.Sample(linear, uvLast); 但是这一步可能会因为遮挡关系的变化而出现错误（e.g. 一个物体在上一帧被其他物体挡住，这一帧因为镜头的移动而出现在屏幕上，此时是无法通过上一帧的信息去对原本被挡住的物体进行平滑的）。因此可以在采样点周围判断深度，取距离镜头最近的点来采样 Motion Vector 的值。\n4. 历史修正\n由于物体在不断运动，随时可能发生遮挡剔除，或是光照发生剧烈变化，此时重投影后拿到的历史帧数据是完全错误的，如果还是直接混合，就会产生“鬼影”。\n解决方法——邻域钳制：\n核心思想：如果历史帧的像素合法，那么就一定不会偏离当前像素的邻域太远。\nClamp：\n以当前像素为中心，采样一个 3x3 的像素矩阵，找出其中颜色的最大值与最小值，作为钳制的范围；此时 C_max 与 C_min 构成了一个 AABB，代表颜色的合法范围。\n通过运动矢量得到上一帧的像素颜色值，强行将其限制在 AABB 中：\n1float3 ClampedHistory = clamp(HistoryColor, ColorMin, ColorMax); 但是 Clamp 可能会导致极其不自然的颜色断层，所以可以使用 Clipping。\nClipping：\n计算得到当前帧的中心像素与上一帧像素的连线，并求得这条线段与 AABB 的交点，将其作为混合结果。\n四：超分辨率抗锯齿 1. DLSS (Deep Learning Super Sampling) TAA 效果的好坏，基本依赖于“历史修正”这一步做得好不好，然而物理世界中的现象有时是无法通过手写代码来建立完美的数学模型的，而 AI 在这里的优势就体现出来了：NVDIA 通过向超大型计算机输入大量超高分辨率的图像作为真值，让一个卷积神经网络（CNN）去学习“如何在运动、遮挡、光影剧变的情况下，完美重构出无锯齿的高清图像\u0026quot; ，其判断精度远高于人为的判断\nDLSS 的核心逻辑是：低分辨率渲染 -\u0026gt; 神经网络时空重构 -\u0026gt; 高分辨率输出\n硬件层渲染：引擎以较低的分辨率（例如 1080p）渲染场景的所有 G-Buffer 和着色。由于分辨率低，光栅化和光线追踪的计算开销暴跌，帧率得以大幅提升\n亚像素抖动：与 TAA 相同，低分辨率渲染时相机依然带有微小的亚像素 Jittering\n硬件加速输入：将低分辨率的当前帧颜色、深度图、运动矢量以及高分辨率的历史缓冲区一同送入 GPU 专属的 Tensor Cores（张量核心）\nAI 重构：神经网络利用其庞大的参数，在空间（上下左右）和时间（前后多帧）两个维度同时进行高级插值与过滤，最终输出一张没有锯齿、细节甚至超越原生渲染的图像（例如 4K）\n2. DLAA（Deep Learning Anti-Aliasing） DLAA 相当于是去除了”空间放大“操作的 DLSS\n运行机制：引擎直接以原生目标分辨率（如 4K 渲染 + 4K 输出）运行。它把完整的高清当前帧、高清深度、高清速度图送入神经网络。AI 不再需要无中生有地猜测缺失的像素，它所有的算力都被毫无保留地倾注在“如何在时间轴上做到最完美的、绝无鬼影与蠕动的顶级抗锯齿”\n优点：细节保留更加完整，同时彻底消除了 TAA 的动态模糊感\n**代价：**在帧率上可能不如 DLSS\n五：光线追踪/路径追踪中的抗锯齿 抖动采样（Jitter Sampling） 在光线追踪中，通常不需要挂载一个复杂的 TAA Pass，只需要在生成主射线时，对其在屏幕空间中穿过的 uv 坐标添加一个微小的抖动；随着 SPP（ Samples Per Pixel ）的累加求均值，锯齿会随着噪点一起消失\n","permalink":"https://jiat-t.github.io/posts/anti-aliasing/","summary":"整理 SSAA、MSAA、FXAA、SMAA、TAA、DLSS/DLAA 以及路径追踪中的抗锯齿思路。","title":"抗锯齿技术"},{"content":"这篇笔记收录了 IBL 相关的三张手写整理图，内容包括离线 IBL、实时 IBL、Split Sum Approximation、Irradiance Map 和球谐函数。\n","permalink":"https://jiat-t.github.io/posts/ibl-notes/ibl-notes/","summary":"整理 Image-Based Lighting、Split Sum Approximation、Irradiance Map 与球谐函数相关手写笔记。","title":"IBL相关笔记"},{"content":"一：单例模式 定义：一个类在全局只有一个实例对象，这个类自行实例化后向整个系统提供这个实例\n实现：禁止拷贝与赋值，防止破坏实例的唯一性\n缺点：全局状态强，调试困难\n懒汉式单例模式： 1class Singleton 2{ 3public: 4 // 在第一次调用 getSingleton() 才初始化 5\tstatic Singleton* getSingleton() 6\t{ 7\t// 局部静态变量 8\t// 在程序结束后自动释放内存 9\t// 防止内存泄漏 10 // 同时也确保了线程安全 11\tstatic Singleton instance; 12\treturn \u0026amp;instance; 13\t} 14 15private: 16\tSingleton() {} 17\t~Singleton() {} 18\tSingleton(const Singleton\u0026amp;); 19\tSingleton\u0026amp; operator=(const Singleton\u0026amp;); 20}; 饿汉式单例模式： 1class Singleton 2{ 3public: 4 static Singleton* getSingleton() 5 { 6 return \u0026amp;instance; 7 } 8 9private: 10 Singleton() {} 11 ~Singleton() {} 12 Singleton(const Singleton\u0026amp;); 13 Singleton\u0026amp; operator=(const Singleton\u0026amp;); 14 15 // 饿汉式的实例是定义在类外的 16 // 在 main() 之前进行初始化 17 static Singleton instance; 18}; 常见问题：\n为什么不直接使用全局变量？\n因为使用单例可以控制初始化时机（static 变量在第一次调用时才被初始化），并且单例可以对接口进行封装\n二：工厂模式 定义：\n","permalink":"https://jiat-t.github.io/posts/design-modes/","summary":"整理单例模式与工厂模式的基础定义、实现方式和常见问题。","title":"Design Modes"},{"content":"一：DX12 相较于 OpenGL 之类的旧 API 有什么优势 核心优势：更底层、更显式、更加适配多线程与现代 GPU 架构。DX12 将旧 API 中许多底层工作交还给应用层，从而使得引擎能够自己控制资源、同步以及命令提交，减少了驱动带来的开销，也提升了程序的可预测性。\nCPU 开销更低：\n在 OpenGL / DX11 中，诸如资源同步、内存管理、命令验证等操作都是由驱动自己完成的，而在 DX12 中需要自己手动地进行显式管理，比如 CPU 与 GPU 的 Fence 设置、使用 Descriptor Heap 绑定描述符、通过 Command List 录制命令等。 这样使得驱动需要去猜的东西变少了，CPU 端的成本更加可控。 多线程的命令录制更好：\nOpenGL / DX11 的命令录制很容易集中在一个主线程上，DX12 支持同时利用多个线程进行命令录制，因此引擎可以将阴影 Pass、后处理、场景遍历等分配给多个线程生成 GPU 命令，降低 CPU 主线程的处理压力。 资源与内存控制更加显式：\nDX12 允许程序员对资源生命周期、堆内存分配等手动进行管理，而旧 API 都是由驱动自动进行管理。 状态切换更加高效：\nDX11 / OpenGL 都是传统的状态机模型，状态多、变化频繁，因此驱动需要不断地去进行验证；DX12 使用的是 PSO（Pipeline State Object），将大量管道状态预先打包，之后在运行期中只需要对创建好的 PSO 进行绑定即可，减少了运行期的状态切换与验证成本。 同步更加可控：\n在旧 API 中，许多同步操作是隐式的，所以可能会出现“上一帧使用某个资源，下一帧又需要对其进行写入，于是驱动偷偷插入等待、重命名等操作来避免冲突”的情况。 DX12 的同步更加显式： CPU / GPU 用 Fence。 GPU 内部资源状态用 ResourceBarrier。 多 Queue 之间也要显式同步。 补充：缺点是现代 API 的代价是复杂度更高，DX12 把原本由驱动完成的工作交由程序员手动完成，如果处理不当，DX12 可能会比 DX11 等旧 API 还要慢。\n二：DX12 是如何将一帧绘制到屏幕上的 获取当前的 Back Buffer：\nSwap Chain 中包含多个缓冲区，当前帧需要绘制到其中一个 Back Buffer 上。 CPU 录制命令：\nCPU 先重置 Command Allocator 和 Command List，再将绘制操作逐个录制。 资源状态切换：\nBack Buffer 初始通常处于 PRESENT 状态，需要使用 Resource Barrier 将其切换至 RENDER_TARGET 状态，这样 GPU 才能往里面写入颜色。 设置渲染目标：\n为当前的 Back Buffer 绑定 RTV，如果需要深度测试，还要绑定 DSV。 清屏：\n将当前颜色缓冲区与深度缓冲区清空。 设置渲染管线：\n对 PSO、Root Signature、顶点缓冲 / 索引缓冲、常量缓冲、纹理、采样器、Viewport 等进行绑定。 发出 Draw Call：\n之后 GPU 会经过顶点着色、光栅化、像素着色等操作，将结果写入 Back Buffer。 资源状态切换：\n将 Back Buffer 从 RENDER_TARGET 切回 PRESENT，准备交给显示系统。 提交命令至 GPU：\nCPU 关闭 Command List，通过 Command Queue 将命令提交至 GPU 进行执行。 显示：\n调用 SwapChain-\u0026gt;Present(...)，DXGI 会将渲染好的 Back Buffer 交给显示系统，最终显示在屏幕上。 Fence 同步：\nCPU 通过 Fence 确认 GPU 已经绘制完成这一帧，此时才可以复用这一帧的 Back Buffer。 三：Command Queue 的三种类型 DX12 将 GPU 工作拆成不同队列，方便图形、计算、拷贝异步执行，但队列之间仍需显式同步\nDirect（Graphics） Queue：\n最通用、最强大的队列\n支持所有 GPU 操作，如 Draw（绘制）、Compute（计算）、Copy（资源转移）、Resource Barrier（资源状态转换）等\nCompute Queue：\n专用于高性能计算任务\n主要支持 Compute Shader 指令和资源复制/转移操作，通常不支持绘制等图形相关指令（如光栅化）；适用于物理模拟、GPU 粒子系统等\nCopy Queue：\n原笔记（优先级表述待复核）\n优先级最高、开销最小的通道，用于数据传输\n仅支持拷贝、填充、清空等简单的内存操作，用于主机到显存的资源上传、显存内的数据移动等\n来源：语雀原笔记\n","permalink":"https://jiat-t.github.io/posts/dx12/","summary":"整理 DX12 相较于旧图形 API 的优势，以及一帧内容从命令录制到 Present 的基本流程。","title":"DX12 杂谈"},{"content":"\n","permalink":"https://jiat-t.github.io/posts/space-transformation-matrix-derivation/space-transformation-matrix-derivation/","summary":"空间变换矩阵推导相关手写笔记。","title":"空间变换矩阵推导"},{"content":"一：Whitted-style 光线追踪 流程：\n从相机发出主光线 命中物体之后根据材质种类继续发射： 反射光线 折射光线 阴影光线 最后通过递归计算所有光线对像素的贡献值 主要用于模拟直接光照与理想情况下的反射、折射现象，对于真实世界的漫反射、全局光照、颜色渗透等并不擅长，因为它的路径通常是确定的：遇到镜面就反射，遇到玻璃就折射。\n二：Monte Carlo 路径追踪 流程：\n从相机发出射线 命中物体后根据 BRDF 随机选择次级射线发射方向 漫反射方向 高光方向 透射方向 \u0026hellip;\u0026hellip; 经过多次采样之后求得平均值 因为光线传播方向的多样性，路径追踪可以很好地模拟诸如漫反射间接光、软阴影、全局光照等现象。\n三：简单对比 项目 递归式光线追踪 路径追踪 代表算法 Whitted Ray Tracing Monte Carlo Path Tracing 光线传播 反射、折射递归分支 随机采样一条或多条路径 主要效果 镜面、玻璃、阴影 全局光照、软阴影、间接光 漫反射间接光 通常不自然支持 天然支持 结果 比较干净 有噪声，需要采样收敛 成本 相对低 通常更高 物理真实性 有限 更接近物理真实 ","permalink":"https://jiat-t.github.io/posts/others/witted-and-mote-carlo/","summary":"\u003ch3 id=\"一whitted-style-光线追踪\"\u003e一：Whitted-style 光线追踪\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e流程：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从相机发出主光线\u003c/li\u003e\n\u003cli\u003e命中物体之后根据材质种类继续发射：\n\u003cul\u003e\n\u003cli\u003e反射光线\u003c/li\u003e\n\u003cli\u003e折射光线\u003c/li\u003e\n\u003cli\u003e阴影光线\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e最后通过递归计算所有光线对像素的贡献值\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e主要用于\u003cstrong\u003e模拟直接光照与理想情况下的反射、折射现象\u003c/strong\u003e，对于真实世界的漫反射、全局光照、颜色渗透等并不擅长，因为它的路径通常是确定的：遇到镜面就反射，遇到玻璃就折射。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"二monte-carlo-路径追踪\"\u003e二：Monte Carlo 路径追踪\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e流程：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e从相机发出射线\u003c/li\u003e\n\u003cli\u003e命中物体后根据 BRDF 随机选择次级射线发射方向\n\u003cul\u003e\n\u003cli\u003e漫反射方向\u003c/li\u003e\n\u003cli\u003e高光方向\u003c/li\u003e\n\u003cli\u003e透射方向\u003c/li\u003e\n\u003cli\u003e\u0026hellip;\u0026hellip;\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e经过多次采样之后求得平均值\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e因为光线传播方向的多样性，路径追踪可以\u003cstrong\u003e很好地模拟诸如漫反射间接光、软阴影、全局光照等现象\u003c/strong\u003e。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"三简单对比\"\u003e三：简单对比\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e项目\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e递归式光线追踪\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e路径追踪\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e代表算法\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWhitted Ray Tracing\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMonte Carlo Path Tracing\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e光线传播\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e反射、折射递归分支\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e随机采样一条或多条路径\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e主要效果\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e镜面、玻璃、阴影\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e全局光照、软阴影、间接光\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e漫反射间接光\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e通常不自然支持\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e天然支持\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e结果\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e比较干净\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e有噪声，需要采样收敛\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e成本\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e相对低\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e通常更高\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e物理真实性\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e有限\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e更接近物理真实\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e","title":"Whitted-style 光线追踪与 Monte Carlo 路径追踪的主要区别"},{"content":"\n","permalink":"https://jiat-t.github.io/posts/shadow-map-derivation/shadow-map-derivation/","summary":"平面阴影矩阵推导相关手写笔记。","title":"平面阴影矩阵推导"},{"content":"核心思想：****阻止栈资源的释放\n当编译器在栈上分配资源时，必须确保其能够在离开作用域时通过调用析构函数被释放。如果人为指定析构函数不可见，那么编译器就会报错，从而阻止了栈上的资源分配。也就是说，需要**破坏编译器在在栈上自动管理生命周期的能力**：\nprivate ：\nHeapOnly() = default;\n此时类对象位于堆上，需要通过指针进行管理，因此在类中定义一个“静态工厂函数”：\nstatic std::unique_ptr create(){ return std::unique_ptr(new HeapOnly()); }\n二：只能在栈上创建对象的类 核心思想：禁止一切形式的 new 操作\n当我们在外部使用“new T（）”创建类对象时，编译器会在类或全局作用域下寻找 new 的重载，如果将 new 标记为 delete，就能从源头切断堆上的分配：\n// 禁止单个对象的堆分配\nvoid* operator new(std::size_t)= delete;\nvoid operator delete(void*)= delete;\n// 禁止数组形式的堆分配\nvoid* operator new[](std::size_t) = delete;\nvoid operator delete[](void*) = delete;\n","permalink":"https://jiat-t.github.io/posts/others/how-to-create-a-heaponly-or-stackonly-class/","summary":"\u003cp\u003e\u003cstrong\u003e核心思想：****\u003cfont style=\"background-color:#FBDE28;\"\u003e阻止栈资源的释放\u003c/font\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e当编译器在栈上分配资源时，必须确保其能够在离开作用域时通过调用析构函数被释放。如果人为指定析构函数不可见，那么编译器就会报错，从而阻止了栈上的资源分配。也就是说，需要**\u003cu\u003e破坏编译器在在栈上自动管理生命周期的能力\u003c/u\u003e**：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eprivate ：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHeapOnly() = default;\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e此时类对象位于堆上，需要通过指针进行管理，因此在类中定义一个“静态工厂函数”：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003estatic std::unique_ptr\u003cHeapOnly\u003e create(){ return std::unique_ptr\u003cHeapOnly\u003e(new HeapOnly()); }\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"二只能在栈上创建对象的类\"\u003e\u003cfont style=\"color:#117CEE;\"\u003e二：只能在栈上创建对象的类\u003c/font\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e核心思想：禁止一切形式的 new 操作\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e当我们在外部使用“new T（）”创建类对象时，编译器会在类或全局作用域下寻找 new 的重载，如果将 new 标记为 delete，就能从源头切断堆上的分配：\u003c/p\u003e\n\u003cp\u003e// 禁止\u003cstrong\u003e单个对象\u003c/strong\u003e的堆分配\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003evoid* operator new(std::size_t)= delete;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003evoid operator delete(void*)= delete;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e// 禁止\u003cstrong\u003e数组形式\u003c/strong\u003e的堆分配\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003evoid* operator new[](std::size_t) = delete;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003evoid operator delete[](void*) = delete;\u003c/code\u003e\u003c/strong\u003e\u003c/p\u003e","title":"如何构造只能在堆/栈上创建对象的类"},{"content":"一：Linear Blending 代码实现：\n1 2float3 normal_1 = tex2D(normal_tex1, uv).xyz * 2.0f - 1.0f; 3 4float3 normal_2 = tex2D(normal_tex2, uv).xyz * 2.0f - 1.0f; 5 6float3 n = normalize(normal_1 + normal_2); 这是一种非常暴力的方法，实现起来也是最简单的（相加后再归一化即可）；虽然最终可以实现混合法线贴图的效果，但是会导致混合后的法线变平，细节也就会更加模糊\n产生这种结果的原因有两个，可以分别从几何维度与物理维度的方向来分析：\n1.几何维度：\n法线本质上是定义在球面上的向量，当我们使用线性的公式对两个法线向量进行混合时，计算的路径实际上是沿着“弦”而非“弧”进行的；这会导致两个法线向量相加得到的中间向量的模始终小于 1，虽然可以通过normalize 将模长拉回至 1，但这又会使得“** 角速度非均匀** ”，表现在混合后的法线上就是：混合结果在中间部分的变化比两端更慢，并且高频的法线会在这种非线性的映射中被抹平，产生“细节模糊”；细节模糊的另一个原因是——当两个法线的方向差异过大（接近180度，也就是反向）时，这样的线性混合得到的结果会趋近于垂直向上，丢失了原有的细节\n2.物理维度：\n法线贴图存储的并不是颜色，而是物体表面的偏导数（斜率），如果使用处理颜色的办法去混合两个法线，只会导致数据的平均，而不是细节的叠加\ne.g. 有两张法线贴图，一张代表“砖块的粗糙纹理”，另一张代表“划痕”，线性混合的行为逻辑是：“我有 50% 像砖块，50% 像划痕”， 但真实的逻辑应该是：“在砖块的斜率基础上，叠加划痕的斜率”\n代码实现：\n1 2float3 normal_1 = tex2D(normal_tex1, uv).xyz * 2.0f - 1.0f; 3 4float3 normal_2 = tex2D(normal_tex2, uv).xyz * 2.0f - 1.0f; 5 6float3 n = normal_1 \u0026lt; 0.5 ? 2 * normal_1 * normal_2 : 1 – 2 * (1 - normal_1) * (1 - normal_2); 7 8n = normalize(n * 2 - 1); 相比于线性混合，Overlay Blending 是对两张法线贴图进行了类似 PS 中“正片叠底”的处理，虽然 overlay 对于增强二维图像的对比度又很好的效果，但是法线贴图本身并不应该被当作颜色贴图进行处理；overlay 并不理解法线贴图存储的是斜率信息，因此他会对x、y、z分量无差别地进行非线性缩放，这会导致严重的数学错误与视觉伪影\n三：Partical Derivative Blending 代码实现：\n1 2float3 normal_1 = tex2D(texBase, uv).xyz*2 - 1; 3 4float3 normal_2 = tex2D(texDetail, uv).xyz*2 - 1; 5 6float2 pd = normal_1.xy/normal_1.z + normal_2.xy/normal_2.z; //Add the PDs 7 8float3 n = normalize(float3(pd, 1)); 根据导数的线性性质：\n$$\r\\frac{\\partial F_{total}}{\\partial x} = \\frac{\\partial f_1}{\\partial x} + \\frac{\\partial f_2}{\\partial x}\r$$可以得知要想混合两个法线，需要对它们在切空间中对应的表面梯度进行线性累加，然后将合成后的梯度重新投影回法线空间\n虽然这在数学上的定义是完美的，但实际的应用会涉及到两次除法以及一次 normalize，开销比较大，所以在离实时渲染中一般不会直接使用这个方法，但是在离线渲染中，由于编译器的优化，这几步对于整个的渲染流程的性能的影响微乎其微，所以用它可以得到非常好的混合法线质量\n","permalink":"https://jiat-t.github.io/posts/others/normal-combination/","summary":"\u003ch3 id=\"一linear-blending\"\u003e\u003cfont style=\"color:#117CEE;\"\u003e一：Linear Blending\u003c/font\u003e\u003c/h3\u003e\n\u003cp\u003e代码实现：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e1\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e2\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etex2D\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_tex1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euv\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003exyz\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mf\"\u003e2.0f\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mf\"\u003e1.0f\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e3\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e4\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etex2D\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_tex2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euv\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003exyz\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mf\"\u003e2.0f\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mf\"\u003e1.0f\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e5\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e6\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enormalize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e这是一种非常暴力的方法，实现起来也是最简单的（相加后再归一化即可）；虽然最终可以实现混合法线贴图的效果，但是会导致\u003cstrong\u003e混合后的法线变平，细节也就会更加模糊\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e产生这种结果的原因有两个，可以分别从\u003cstrong\u003e几何维度与物理维度\u003c/strong\u003e的方向来分析：\u003c/p\u003e\n\u003cp\u003e\u003cfont style=\"background-color:#F1A2AB;\"\u003e1.几何维度：\u003c/font\u003e\u003c/p\u003e\n\u003cp\u003e法线本质上是定义在球面上的向量，当我们使用线性的公式对两个法线向量进行混合时，计算的路径实际上是沿着“弦”而非“弧”进行的；这会导致\u003cstrong\u003e两个法线向量相加得到的中间向量的模始终小于 1\u003c/strong\u003e，虽然可以通过normalize 将模长拉回至 1，但这又会使得“** 角速度非均匀**  ”，表现在混合后的法线上就是：\u003cu\u003e混合结果在中间部分的变化比两端更慢，并且高频的法线会在这种非线性的映射中被抹平\u003c/u\u003e，产生“\u003cstrong\u003e细节模糊\u003c/strong\u003e”；细节模糊的另一个原因是——当两个法线的方向差异过大（接近180度，也就是反向）时，这样的\u003cu\u003e线性混合得到的结果会趋近于垂直向上，丢失了原有的细节\u003c/u\u003e\u003c/p\u003e\n\u003cp\u003e\u003cfont style=\"background-color:#F1A2AB;\"\u003e2.物理维度：\u003c/font\u003e\u003c/p\u003e\n\u003cp\u003e法线贴图存储的并不是颜色，而是物体表面的偏导数（斜率），如果使用处理颜色的办法去混合两个法线，\u003cstrong\u003e只会导致数据的平均，而不是细节的叠加\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cem\u003ee.g. 有两张法线贴图，一张代表“砖块的粗糙纹理”，另一张代表“划痕”，线性混合的行为逻辑是：“我有 50% 像砖块，50% 像划痕”， 但真实的逻辑应该是：“在砖块的斜率基础上，叠加划痕的斜率”\u003c/em\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e代码实现：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e1\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e2\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etex2D\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_tex1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euv\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003exyz\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mf\"\u003e2.0f\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mf\"\u003e1.0f\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e3\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e4\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etex2D\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_tex2\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euv\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003exyz\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mf\"\u003e2.0f\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mf\"\u003e1.0f\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e5\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e6\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e  \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e \u003cspan class=\"mf\"\u003e0.5\u003c/span\u003e \u003cspan class=\"o\"\u003e?\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"nl\"\u003enormal_2\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"err\"\u003e–\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e7\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e8\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enormalize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003en\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e相比于线性混合，Overlay Blending 是对两张法线贴图进行了类似 PS 中“正片叠底”的处理，虽然 overlay 对于增强二维图像的对比度又很好的效果，但是法线贴图本身并不应该被当作颜色贴图进行处理；overlay 并不理解法线贴图存储的是斜率信息，因此他会对x、y、z分量无差别地进行非线性缩放，这会导致严重的数学错误与视觉伪影\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"三partical-derivative-blending\"\u003e\u003cfont style=\"color:#117CEE;\"\u003e三：Partical Derivative Blending\u003c/font\u003e\u003c/h3\u003e\n\u003cp\u003e代码实现：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e1\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e2\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etex2D\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etexBase\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euv\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003exyz\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e3\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e4\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etex2D\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003etexDetail\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003euv\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003exyz\u003c/span\u003e\u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"mi\"\u003e2\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e5\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e6\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat2\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003exy\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_1\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ez\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003exy\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003enormal_2\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ez\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e//Add the PDs\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e7\u003c/span\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"ln\"\u003e8\u003c/span\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e \u003cspan class=\"n\"\u003en\u003c/span\u003e  \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003enormalize\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003efloat3\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003epd\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e根据导数的线性性质：\u003c/p\u003e\n$$\r\n\\frac{\\partial F_{total}}{\\partial x} = \\frac{\\partial f_1}{\\partial x} + \\frac{\\partial f_2}{\\partial x}\r\n$$\u003cp\u003e可以得知要想混合两个法线，需要\u003cstrong\u003e对它们在切空间中对应的表面梯度进行线性累加，然后将合成后的梯度重新投影回法线空间\u003c/strong\u003e\u003c/p\u003e","title":"如何混合法线贴图"},{"content":"这篇笔记收录了蒙特卡洛积分推导的手写整理图。\n","permalink":"https://jiat-t.github.io/posts/monte-carlo-integral-notes/monte-carlo-integral-notes/","summary":"整理蒙特卡洛积分推导相关手写笔记。","title":"蒙特卡洛积分推导笔记"},{"content":"这篇笔记收录了重要性采样相关的两张手写整理图。\n","permalink":"https://jiat-t.github.io/posts/importance-sampling-notes/importance-sampling-notes/","summary":"整理重要性采样与多重重要性采样相关手写笔记。","title":"重要性采样笔记"},{"content":"这篇笔记收录了 BRDF 相关公式的手写整理图。\n","permalink":"https://jiat-t.github.io/posts/brdf-notes/brdf-notes/","summary":"整理 BRDF 与常见微表面反射模型相关手写笔记。","title":"BRDF 相关公式笔记"},{"content":"模板：\n回溯模板\n1class Solution 2{ 3private: 4 vector\u0026lt;Type_\u0026gt; res; 5 Type_ path; 6public: 7 void backtracking(输入参数) 8 { 9 if (终止条件) 10 { 11 res.push_back(path); 12 return; 13 } 14 15 for (本层要使用的集合) 16 { 17 处理节点; 18 backtracking(...); 19 回溯，即撤销之前的结果; 20 } 21 } 22 23 vector\u0026lt;Type_\u0026gt; func(输入参数) 24 { 25 backtracking(...); 26 return res; 27 } 28}; 原文：回溯\n","permalink":"https://jiat-t.github.io/leetcode/back-track/backtracking-overview/","summary":"记录回溯算法的结果集合、当前路径、终止条件以及撤销操作的 C++ 模板。","title":"回溯算法模板"},{"content":"榎 aaaa\n","permalink":"https://jiat-t.github.io/posts/hello/","summary":"\u003cp\u003e榎 aaaa\u003c/p\u003e","title":"Hello"},{"content":"这里是整理的一些c++基础知识\u0026hellip;.\n就这样吧\u0026hellip;.\n语法\n1.C++的三大特性 封装，继承，多态（同时也是面向对象编程的三大特性）\n1).简要解释一下上面三者 ·封装：隐藏内部具体的实现细节，对外部只暴露必要的接口来访问对象\n·继承：允许一个类从另一个类继承属性和方法，从而实现代码复用和层次化抽象\n·多态：同一个函数名拥有多种状态，或是同一个接口具有不同的行为\nLINK：\n如何理解面向对象编程三大特性\u0026mdash;\u0026mdash;封装、继承、多态\n2).多态的类别 ·运行时多态（动态联编）：通过继承和虚函数实现\n·编译时多态（静态联编）：通过重载和模板实现\n2.虚函数 简单来说，虚函数就是允许在基类（父类）中定义一个函数，并允许派生类（子类）对其进行重写 (Override)\n1).虚函数的实现机制 ·通过虚函数表实现，虚函数表是一个由编译器生成的静态数组，位于类的全局静态内存区中，包含了一个类所有的虚函数的地址（即该类所有虚函数的入口）\n·在类对象（不是类！）内存空间的头部，有一个虚函数表指针，它指向该类虚函数表的首地址\n·当子类对象对父类虚函数进行重写的时候，虚函数表中相应的虚函数的地址会被更改为重写后的虚函数地址\n2).虚函数调用是在编译时确定还是运行时确定的？如何确定调用哪个函数？ ·一般来说是在运行时确定，可以通过虚函数表中的地址来确定调用什么函数\n3).能否在基类的构造函数和析构函数中调用虚函数？ ·在语法上来说没有问题，但是在实际调用时，无法实现多态的效果，因为构造函数会先初始化基类的成员，而此时子类的成员还是一片空白，如果这时使用子类中还未被初始化的成员，会出现错误；同理，析构函数是先从子类开始析构，如果在基类的析构函数中调用虚函数的话，因为此时子类已经被析构，是无法调用其特有成分的\n4).当一个类需要被继承时，它的析构函数必须是虚函数 如果基类的析构函数不为虚函数，那么当对派生类对象进行删除（delete）操作时，就只会调用基类的析构函数，这会导致内存泄漏；\n而当基类包含虚析构函数时，在同样的情况下，编译器会先查询虚函数表，并调用对应的析构函数\n3.C++的内存管理 1).C++有哪些内存区域 ·栈：由编译器自动分配与释放，多用于存储局部变量；速度极快，但空间有限（通常只有几MB）\n·堆：需要手动控制生存周期， 从 new 或 malloc 开始，直到 delete 或 free；空间极大，分配位置不连续\n·全局/静态存储区： 存储全局变量和静态变量，在程序启动时分配，程序退出时才释放 ；\n.data 段：存储已初始化的全局/静态变量。\n.bss 段：存储未初始化的全局/静态变量（自动初始化****为 0）。\n·常量存储区： 存储只读数据\n·自由存储区：通过new和delete分配和释放空间的内存，具体实现可能是堆或者内存池\n·代码区：存储经过编译之后的机器指令（二进制代码）\nLink:\nC++内存管理 - 还没放弃的老张 - 博客园\n2).栈 与 堆 的区别 1.堆的内存空间大，栈的内存空间小\n2.堆的分配效率低，栈的分配效率高\n3.分配和释放堆内存时会产生内存碎片，而栈不会\n4.堆中的内存需要手动申请与释放，栈的内存由OS自动申请与释放\n5.堆的地址从下往上，栈的地址从上往下\n3).C++和C分别使用什么函数来做内存的分配和释放？有什么区别，能否混用？ C++使用new/delete分配与释放内存，C使用malloc/free分配与释放内存\n区别：\n1.使用malloc分配内存时需要指定内存大小，而new不需要\n2.malloc从堆中分配内存，new从自由存储区分配内存\n3.对于类对象，new能够调用构造和析构函数，而malloc不行\n4.使用new分配内存返回的是相应的类型指针，而malloc先返回的是void*，之后再强转为类型指针\n4).什么是内存对齐？为什么要内存对齐？如何对齐？ **含义：**内存对齐就是C++数据结构中的成员，其内存地址是否为对齐字节大小的倍数\n**原因：**为了提高效率，计算机在内存中取数据时是按照一个固定长度的，比如在32位机上，计算机每次只取32位的数据，也就是4字节。如果不进行对齐，就有可能出现一个数据出现在两个地址上的情况，通过两次取出操作之后，还需要经过掩码和移位等操作，效率极低；同时，也可能会造成内存浪费（产生了不必要的内存填充）\n做法：申明成员变量时，字节对齐的数据依次申明，尽量将小数据成员放在一起，减少内存填充产生的浪费\n4.C++的类型转换 1).类型转换的种类 1.static_cast（最常用）：隐式类型转换，用于转换C++内置的基本数据类型（enum, struct, int, float, double, char\u0026hellip;.)，但是不能进行无关类型的转换（如：非基类和子类）以及包含底层const的对象\n2.const_cast：用于增加/去除const限定符，只能用于指针或引用\n3.dynamic_cast：动态类型转换，主要用于将基类指针转换为派生类指针或引用的向下类型转换（也可以是向上类型转换)。如果指针转换失败则返回NULL，引用转换失败返回bad_cast异常。因为其在运行时会检查对象的虚表（安全性检查），因此基类中一定要存在虚函数，否则编译不通过\n4.reinterpret_cast：重新解释转换，在编译期，将一段数据的二进制位转换成完全不同的类型，但是不改变其值，和C的强制转换差不多，可以将一个类型转换为其他任何想要的类型，比较不安全\n2).static_cast与dynamic_cast的异同： 同：两者都会进行类型安全检查\n异：static_cast在编译期进行检查，dynamic_cast在运行期检查；\nstatic_cast不需要基类拥有虚函数，dynamic_cast需要被转换对象的基类拥有虚函数\n3).dynamic_cast的原理？如何自行实现？ dynamic_cast是唯一一个在运行时进行类型检查的转换操作符，它的核心机制叫做RTTI（Run-Time Type information）\nPS：这里没写完，之后能看懂了再补上\nLINK：\n[C++] - dynamic_cast介绍及工作原理、typeid、type_info_dynamiccast-CSDN博客\n5.C++的智能指针 智能指针是为了解决内存泄漏问题，它的本质是一个类，在函数结束时会自动调用析构函数并释放内存空间\n1).类型： 1.unique_ptr(独占指针）：在同一时间段，只能有一个智能指针指向对象，无法被拷贝，只能移动。内存开销极低\n2.shared_ptr(共享指针）：多个共享指针可以指向同一个对象，采用引用计数的方法，当最后一个共享指针被销毁时才释放对象的内存空间。内存开销中等（因为既包含了指向数据的数据指针，又包含了指向用于计数的控制块的指针）\n3.weak_ptr(弱指针）：通常与shared_ptr一起出现，用于解除两个shared_ptr互相引用时的死锁问题。内存开销低\n2).shared_ptr的实现原理： 构造函数：shared_ptr指向一个对象时，引用计数置1；同理，每当有一个shared_ptr被销毁时，引用计数\u0026ndash;；\n拷贝构造函数：每当有新的shared_ptr指向同一个对象时，引用计数++\n赋值运算符：=左边的shared_ptr的引用计数减一，右边的则加一————e.g.ptr1 = ptr2，这意味着ptr1之前指向的控制块的引用计数减一，同时ptr2指向的控制块的引用计数加一；如果左边的引用计数减为0，则销毁所指向的对象，并释放内存空间\n3).weak_ptr是如何解决shared_ptr的循环引用问题的： 在shared_ptr的底层控制块中，其实存在着两个计数器：\n**shared_count**_：_记录有多少个 shared_ptr 拥有该对象。当它变为 0，对象被析构 **weak_count**_：_记录有多少个 weak_ptr 观察该对象 只有当两者同时变为 0 时，存储这些计数的“控制块”本身才会被释放\n回归正题，shared_ptr是“强占有”，只要它指向对象，引用计数就+1，对象就不能被销毁，而weak_ptr是“弱观察”，他也指向对象，但不会增加引用计数\ne.g. A拥有B的shared_ptr，B拥有A的weak_ptr，此时指向A的最后一个shared_ptr被销毁，因为B占有的是weak_ptr，不会使A的引用计数加一，因此A的shared_count降为0，触发析构函数，这会导致存在于A中，指向B的shared_ptr也被析构，使得B的引用计数也降为0，B的析构函数也被调用，它内部的weak_ptr同样会被销毁，当最后一个weak_ptr也被销毁时，weak_count降为0，内存中的控制块才被释放\n4).既然weak_ptr是为了解决shared_ptr的循环引用，那为什么不用raw ptr来解决这个问题： 如果使用了raw ptr，那么在它指向的shared_ptr被销毁之后，会出现 悬空指针 的情况；而weak_ptr会调用lock（）函数，如果所指向的shared_ptr被销毁，当再次使用weak_ptr时，lock（）函数会返回一个空的shared_ptr，其内部是nullptr\n6.C++的说明符 1).类型： 1.const： （1）作用： 用来定义常量，具有不可变性；\n（2）什么是常量指针和指针常量： 常量指针：指针指向的对象是常量，不可更改，但是指针的指向可以改变；\n指针常量：指针本身是一个常量，其指向不可被更改，但是指向的对象可以被改变；\n这里涉及到的是顶层const和底层const的概念：\n顶层const：本身是cosnt类型\n底层const：指向的对象是const类型\n（3）const修饰的函数能否重载： const修饰的函数能够被重载。\nconst修饰的类成员函数既不能改变类的成员变量，也不能调用非const成员变量与非const成员函数，只能使用\nconst对象只能调用const成员函数，非cosnt对象可以调用所有成员函数\n非 const 对象优先选择非 const 函数以保留修改能力，而 const 对象则被严禁越权、仅能调用只读的 const 版本\n2.static： 代表静态的意思，能够对变量与函数进行修饰\n（1）当static作用于文件作用域时，该变量或函数只在本文件可见，其他文件既看不到也无法使用，避免了重定义的问题\n（2）当static作用于函数作用域时，即作为局部静态变量时，只会在第一次调用函数的时候初始化，也只在该函数内可见\n（3）当static用于类的申明时，即静态数据成员和静态成员函数，意味着类中的这些成员是所有类对象共有的\n·static变量什么时候初始化： static在类中申明时不占用内存，只有在第一次执行到该变量的声明处才进行初始化\n文件域的静态变量和类的静态成员变量在main函数执行之前的静态初始化过程中分配内存并初始化，局部静态变量在第一次使用时分配内存并初始化\n3.constexpr： 告诉编译器，在编译阶段就能够确定该常量的值，需要直接得出结果，并将其硬编码进入程序中\n4.extern： 当它作为函数或者全局变量的声明时，提示编译器应当去其他的文件中寻找其定义；\n当它与“C”连用时，告诉编译器按照C语言的规则去编译这段代码，而不是按照C++的规则\n5.explicit： 主要用于类的构造函数，告诉编译器禁止隐式类型转换\n6.volatile： 告诉编译器，每次使用该变量的时候一定要从编译器中真正取出，而不是使用寄存器中的备份\n7.mutable： 允许const成员函数（注意是仅限于类的成员函数）修改被其标记的变量\n8.auto与decltype： 特性 auto decltype 推导依据 变量的初始化值 任意表达式/变量名 初始化 必须初始化 不需要初始化 修饰符处理 默认丢弃 \u0026amp; 和顶层 const 严格保留所有修饰符 主要用途 简化冗长的类型声明 泛型编程（模板）、定义返回类型 9.noexcept 当他出现在函数末尾时，就是在告诉编译器：这个函数绝对不会出现任何异常，如果有，就直接终止程序。在移动构造函数中常常使用到，用于跨越vector的性能陷阱：\n7.左值与右值 1).定义： 左值：具有明确内存地址，有名字，可由用户改变其值的变量，如int，float等等常见的变量；左值具有持久的状态，只有离开了作用域才会被销毁\n右值：临时生成的，没有名字，即将销毁的数据，如字面量40，返回非引用类型的函数调用等等\n2).什么是右值引用： 就是只能绑定到右值的引用，通过“\u0026amp;\u0026amp;”取得；右值引用只能绑定于即将销毁的对象，因此可以自由的移动资源\n3).为什么要使用右值引用： 实现“移动语义”，避免昂贵的拷贝开销；\n使用std：：move()函数可以将一个左值转化为右值引用\n4).什么是拷贝构造函数： 1.定义：用于使用一个已有的类对象去初始化另一个同类型的类对象；再某些情况下，如果使用默认的构造函数，会出现错误，比如：类的成员包含一个指针，如使用默认构造函数，被初始化的对象中就也会出现一个指向相同对象的指针，如果其中的一个类对象被析构，那么其中的指针也会被delete，这样就会造成另一个指针的悬空引用\n2.深拷贝与浅拷贝：\n浅拷贝：仅仅只是简单地复制指向某个对象的指针，而非再拷贝一份对象本身，因此新旧对象仍然是指向同一个地址\n深拷贝：会额外创建一个与原对象一模一样的对象，新旧对象不共享内存\n例（深拷贝）：\n1// int width = 0; 2// int height = 0; 3// float* pixels = nullptr; 4 5// 2. 深拷贝构造函数 (Copy Constructor) 6SimpleImage(const SimpleImage\u0026amp; other) 7{ 8 // TODO: 实现深拷贝逻辑 9 size_t total_pixels = width * height; 10 pixels = new float[total_pixels]; 11 // 使用 STL 算法进行高效的内存块拷贝 12 std::copy(other.pixels, other.pixels + total_pixels, pixels); 13 std::cout \u0026lt;\u0026lt; \u0026#34;[Copy Constructor] Deep copy performed.\\n\u0026#34;; 14} 15 16// 3. 深拷贝赋值运算符 (Copy Assignment Operator) 17SimpleImage\u0026amp; operator=(const SimpleImage\u0026amp; other) 18{ 19 // 提示：考虑 Copy-and-Swap 惯用法 20 // 检查自赋值：防止 img1 = img1 导致自我销毁 21 if (this != \u0026amp;other) 22 { 23 size_t total_pixels = other.width * other.height; 24 25 // 第一步：先分配新内存。如果这里抛出 std::bad_alloc 异常，原对象完好无损。 26 float* new_pixels = new float[total_pixels]; 27 std::copy(other.pixels, other.pixels + total_pixels, new_pixels); 28 29 // 第二步：释放自己旧的内存，防止内存泄露 30 delete[] pixels; 31 32 // 第三步：接管新内存和新状态 33 pixels = new_pixels; 34 width = other.width; 35 height = other.height; 36 37 std::cout \u0026lt;\u0026lt; \u0026#34;[Copy Assignment] Deep copy performed.\\n\u0026#34;; 38 } 39 return *this; 40} 5).什么是移动构造函数： 移动构造函数的参数是一个右值引用，这个函数不需要分配新的内存，而是直接接管传入参数的内存，并且在完成移动之后销毁原对象\n相较于拷贝构造函数，移动构造函数不需要重新分配内存（前者传入的是左值引用），因此性能更高\n1// 注意：强烈建议加上 noexcept 关键字 2SimpleImage(SimpleImage\u0026amp;\u0026amp; other) noexcept 3: width(other.width), 4height(other.height), 5pixels(other.pixels) // 第一步：直接把对方的指针拿过来（浅拷贝指针本身） 6{ 7 // 第二步：将原对象“掏空”，使其处于合法但空的状态 8 other.width = 0; 9 other.height = 0; 10 other.pixels = nullptr; // 致命操作：如果没有这一步，other 析构时会把我们刚抢来的内存删掉！ 11 12 std::cout \u0026lt;\u0026lt; \u0026#34;[Move Constructor] Resource stolen! Zero overhead.\\n\u0026#34;; 13} 8.内联函数与宏 1).内联函数的作用与缺点： 作用：直接在函数调用处展开定义，避免调用函数的开销 缺点：可能造成代码膨胀，尤其是递归函数；此外，内联函数不方便调试，每次修改后要重新编译头文件 2).内联函数与宏的区别： (1).define宏指令在预处理阶段对命令进行替换，inline内联函数在编译期展开函数\n(2).宏指令不会进行类型检查，因此可能出现类型安全问题；而内联函数会在编译期进行类型检查\n(3).使用宏的时候可能要添加许多括号，容易出错\n9.C++11的新特性 1).auto关键字：自动推导变量的类型\n2).移动语义：右值引用、std::move等，消除不必要的拷贝\n3).智能指针：保证内存安全\n4).nullptr代替NULL：NULL的实际定义是 #define NULL 0,因此它实际上是int类型，假如现在有两个重载函\t数，参数类型分别为int和指针，如果想要通过空指针来使用第二个函数，实际却会调用第一个函数；而 nullptr的类型是 std::nullptr_t ，它可以被隐式转换为任何指针（float*， void*\u0026hellip;..），所以也就可以避免函 数重载时出现的一些问题\n5).lambda 表达式：简单来说就是一个匿名函数，可以直接在代码运行的地方直接定义\n6).constexpr：在编译期进行优化\n7).并发编程：引入 std::thread\n8).统一使用大括号 { } 进行初始化\n10.指针和引用的区别 1).指针本质是一个地址，有自己的内存空间；引用只是一个别名\n2).指针可以更改指向的对象；而引用一经初始化之后，就不可更改\n3).指针可以初始化为空指针；引用则必须初始化为一个已有对象的引用\n4).指针可以是多级指针；引用只有一级\n11.什么是重载、重写、隐藏 重载： 同名函数具有不同参数列表，根据传入的不同参数决定使用哪一个函数\n重写（覆盖）： 指的是基类的虚函数被派生类重新定义，重写后的函数的返回值与参数列表必须与基类虚函数一样\n隐藏： 当派生类中出现与基类的成员函数同名的函数时，基类的相应函数就会被隐藏（基类函数不能被声明为virtual）\nSTL、数据结构、算法 ","permalink":"https://jiat-t.github.io/posts/cpp-fundamentals/cpp-fundamentals/","summary":"\u003cp\u003e这里是整理的一些c++基础知识\u0026hellip;.\u003c/p\u003e\n\u003cp\u003e就这样吧\u0026hellip;.\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cstrong\u003e语法\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"1c的三大特性\"\u003e1.C++的三大特性\u003c/h2\u003e\n\u003cp\u003e封装，继承，多态（同时也是面向对象编程的三大特性）\u003c/p\u003e\n\u003ch3 id=\"1简要解释一下上面三者\"\u003e1).简要解释一下上面三者\u003c/h3\u003e\n\u003cp\u003e·封装：隐藏内部具体的实现细节，对外部只暴露必要的接口来访问对象\u003c/p\u003e\n\u003cp\u003e·继承：允许一个类从另一个类继承属性和方法，从而实现代码复用和层次化抽象\u003c/p\u003e\n\u003cp\u003e·多态：同一个函数名拥有多种状态，或是同一个接口具有不同的行为\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLINK：\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://zhuanlan.zhihu.com/p/45445817\"\u003e如何理解面向对象编程三大特性\u0026mdash;\u0026mdash;封装、继承、多态\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"2多态的类别\"\u003e2).多态的类别\u003c/h3\u003e\n\u003cp\u003e·运行时多态（动态联编）：通过继承和虚函数实现\u003c/p\u003e\n\u003cp\u003e·编译时多态（静态联编）：通过重载和模板实现\u003c/p\u003e\n\u003ch2 id=\"2虚函数\"\u003e2.虚函数\u003c/h2\u003e\n\u003cp\u003e简单来说，虚函数就是允许在基类（父类）中定义一个函数，并允许派生类（子类）对其进行\u003cstrong\u003e重写 (Override)\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"1虚函数的实现机制\"\u003e1).虚函数的实现机制\u003c/h3\u003e\n\u003cp\u003e·通过虚函数表实现，虚函数表是一个由编译器生成的静态数组，位于类的全局静态内存区中，包含了一个类所有的虚函数的地址（即该类所有虚函数的入口）\u003c/p\u003e\n\u003cp\u003e·在类对象（不是类！）内存空间的头部，有一个虚函数表指针，它指向该类虚函数表的首地址\u003c/p\u003e\n\u003cp\u003e·当子类对象对父类虚函数进行重写的时候，虚函数表中相应的虚函数的地址会被更改为重写后的虚函数地址\u003c/p\u003e\n\u003ch3 id=\"2虚函数调用是在编译时确定还是运行时确定的如何确定调用哪个函数\"\u003e2).虚函数调用是在编译时确定还是运行时确定的？如何确定调用哪个函数？\u003c/h3\u003e\n\u003cp\u003e\u003cfont style=\"color:rgb(25, 27, 31);\"\u003e·一般来说是在运行时确定，可以通过虚函数表中的地址来确定调用什么函数\u003c/font\u003e\u003c/p\u003e\n\u003cp\u003e\u003cfont style=\"color:rgb(25, 27, 31);\"\u003e\u003c/font\u003e\u003c/p\u003e\n\u003ch3 id=\"3能否在基类的构造函数和析构函数中调用虚函数\"\u003e3).能否在基类的构造函数和析构函数中调用虚函数？\u003c/h3\u003e\n\u003cp\u003e·在语法上来说没有问题，但是在实际调用时，无法实现多态的效果，因为构造函数会先初始化基类的成员，而此时子类的成员还是一片空白，如果这时使用子类中还未被初始化的成员，会出现错误；同理，析构函数是先从子类开始析构，如果在基类的析构函数中调用虚函数的话，因为此时子类已经被析构，是无法调用其特有成分的\u003c/p\u003e\n\u003ch3 id=\"4当一个类需要被继承时它的析构函数必须是虚函数\"\u003e4).当一个类需要被继承时，它的析构函数必须是虚函数\u003c/h3\u003e\n\u003cp\u003e如果基类的析构函数不为虚函数，那么当对派生类对象进行删除（delete）操作时，就只会调用基类的析构函数，这会导致内存泄漏；\u003c/p\u003e\n\u003cp\u003e而当基类包含虚析构函数时，在同样的情况下，编译器会先查询虚函数表，并调用对应的析构函数\u003c/p\u003e\n\u003ch2 id=\"3c的内存管理\"\u003e3.C++的内存管理\u003c/h2\u003e\n\u003ch3 id=\"1c有哪些内存区域\"\u003e1).C++有哪些内存区域\u003c/h3\u003e\n\u003cp\u003e·\u003cu\u003e栈\u003c/u\u003e：由编译器自动分配与释放，多用于存储局部变量；速度极快，但空间有限（通常只有几MB）\u003c/p\u003e\n\u003cp\u003e·\u003cu\u003e堆\u003c/u\u003e：需要手动控制生存周期， 从 \u003ccode\u003enew\u003c/code\u003e 或 \u003ccode\u003emalloc\u003c/code\u003e 开始，直到 \u003ccode\u003edelete\u003c/code\u003e 或 \u003ccode\u003efree\u003c/code\u003e；空间极大，分配位置不连续\u003c/p\u003e\n\u003cp\u003e·\u003cu\u003e全局/静态存储区\u003c/u\u003e： 存储全局变量和静态变量，在程序启动时分配，程序退出时才释放  ；\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e.data 段\u003c/strong\u003e：存储已初始化的全局/静态变量。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e.bss 段\u003c/strong\u003e：存储未初始化的全局/静态变量（自动初始化****为 0）。\u003c/p\u003e\n\u003cp\u003e·\u003cu\u003e常量存储区\u003c/u\u003e： 存储只读数据\u003c/p\u003e\n\u003cp\u003e·\u003cu\u003e\u003cfont style=\"color:rgb(25, 27, 31);\"\u003e自由存储区\u003c/font\u003e\u003c/u\u003e\u003cfont style=\"color:rgb(25, 27, 31);\"\u003e：通过new和delete分配和释放空间的内存，具体实现可能是堆或者内存池\u003c/font\u003e\u003c/p\u003e\n\u003cp\u003e·\u003cu\u003e代码区\u003c/u\u003e：存储经过编译之后的机器指令（二进制代码）\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eLink:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.cnblogs.com/learning-zjx/p/10645659.html\"\u003eC++内存管理 - 还没放弃的老张 - 博客园\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"2栈-与-堆-的区别\"\u003e2).栈 与 堆 的区别\u003c/h3\u003e\n\u003cp\u003e1.堆的内存空间大，栈的内存空间小\u003c/p\u003e","title":"Fundamentals of Cpp：现代 C++ 核心笔记"},{"content":"作业五是实现基础的光线追踪（光线生成与三角形求交）\n1.光线生成：\n目标：将屏幕上（不是屏幕空间！）的2D像素坐标映射为3D世界的一条线\n需要将像素从光栅空间-\u0026gt;NDC空间-\u0026gt;屏幕空间-\u0026gt;相机空间-\u0026gt;世界空间\n（1）*光栅空间-\u0026gt;NDC空间*\n拿到像素的左上角顶点坐标，加0.5得到像素中心坐标， 之后将x，y分别除以width，height，映射到【0，1】区间内\n即：（x + 0.5f) / width; (y + 0.5f) / height;\n（2）*NDC空间-\u0026gt;屏幕空间*\n将坐标从【0，1】映射到【-1，1】区间，\n即：x = 2 * x - 1； y = 1 - 2 * y;\n注：在光栅空间中，y轴向下；而屏幕空间的y轴向上，因此需要用1-\u0026hellip;\n（3）*屏幕空间-\u0026gt;相机空间*\n这里需要引入相机的物理属性 （宽高比和视场角） ，将2D坐标拉伸为3D的相机坐标\n屏幕空间的【-1，1】区间是一个正方形（因为x，y都处于这个区间中），但是屏幕大多是长方形的（如1920x1080），因此需要对x坐标进行拉伸；同时还要根据xy原始的大小对其进行放大\n即： x = x * radio * sacle; y = y * scale;\n（4）*相机空间-\u0026gt;世界空间*\n将上一步得到的坐标（x，y，-1）乘以 世界到相机的旋转矩阵的逆矩阵，即可求得在世界空间的坐标\n但是因为这里的相机位于原点处，且方向看向-z，因此不需要做任何处理\n之后调用castRay（）函数就可得到这一像素在世界空间发出的光线\nloop\n1for (int j = 0; j \u0026lt; scene.height; ++j) 2 { 3 for (int i = 0; i \u0026lt; scene.width; ++i) 4 { 5 // generate primary ray direction 6 float x = i; 7 float y = j; 8 // TODO: Find the x and y positions of the current pixel to get the direction 9 // vector that passes through it. 10 // Also, don\u0026#39;t forget to multiply both of them with the variable *scale*, and 11 // x (horizontal) variable with the *imageAspectRatio* 12 13 // Rasterized Space -\u0026gt; NDC Space 14 float x_ndc = (x + 0.5f) / scene.width; 15 float y_ndc = (y + 0.5f) / scene.height; 16 17 // NDC Space -\u0026gt; Screen Space 18 float x_screen = 2 * x_ndc - 1; 19 float y_screen = 1 - y_ndc * 2; 20 21 // Screen Space -\u0026gt; Camera Space 22 float x_camera = x_screen * imageAspectRatio * scale; 23 float y_camera = y_screen * scale; 24 25 // camera Space -\u0026gt; World Space 26 Vector3f dir = Vector3f(x_camera, y_camera, -1); // Don\u0026#39;t forget to normalize this direction! 27 dir = normalize(dir); 28 framebuffer[m++] = castRay(eye_pos, dir, scene, 0); 29 } 2.光线与三角形求交：\n使用的是 Möller-Trumbore 算法 ：\n射线方程：P = O + t*D三角形内部点：P = V_0 + u*E_1 + vE*_2 联立可得到线性方程组：\n之后可以解得t,u,v的值，u，v是 三角形内部的重心坐标 ，t（tNear）是光线撞上三角形所用的最短的时间\n判断条件：(u，v\u0026lt; 0)或(u + v \u0026gt;1)则交点位于三角形之外，t\u0026lt;0则说明三角形在光线后面，这三种情况都意味着光线未能与三角形相交\nrayTriangleIntersect（）\n1bool rayTriangleIntersect(const Vector3f\u0026amp; v0, const Vector3f\u0026amp; v1, const Vector3f\u0026amp; v2, const Vector3f\u0026amp; orig, 2const Vector3f\u0026amp; dir, float\u0026amp; tnear, float\u0026amp; u, float\u0026amp; v) 3{ 4 // TODO: Implement this function that tests whether the triangle 5 // that\u0026#39;s specified bt v0, v1 and v2 intersects with the ray (whose 6 // origin is *orig* and direction is *dir*) 7 // Also don\u0026#39;t forget to update tnear, u and v. 8 Vector3f e1 = v1 - v0, e2 = v2 - v0, s = orig - v0; 9 Vector3f s1 = crossProduct(dir, e2); 10 11 float S1dotE1 = dotProduct(s1, e1); 12 if (std::abs(S1dotE1) \u0026lt; 0.0001f) return false; 13 float invS1dotE1 = 1 / S1dotE1; 14 15 u = dotProduct(s1, s) * invS1dotE1; 16 if (u \u0026lt; 0.0f || u \u0026gt; 1.0f) return false; 17 18 Vector3f s2 = crossProduct(s, e1); 19 v = dotProduct(s2, dir) * invS1dotE1; 20 if (v \u0026lt; 0.0f || v \u0026gt; 1.0f) return false; 21 22 tnear = dotProduct(s2, e2) * invS1dotE1; 23 if (tnear \u0026gt;= .0f) return true; 24 25 return false; 26} 附：结果图\u0026mdash;\u0026mdash;\n原文：5\n","permalink":"https://jiat-t.github.io/graphics/games101-assignment-5/","summary":"整理从像素坐标生成相机光线的空间变换流程，以及三角形求交的基础实现。","title":"GAMES101 作业 5：基础光线追踪"},{"content":"Game Objects：\nDynamic game objects：角色，载具\u0026hellip;\nStatic game objects：房子，岩石\u0026hellip;.\nEnvironment：天空，植被，地势\u0026hellip;.\nOther object：空气墙，检测区域\u0026hellip;.\n对于一个game object，它既存在Attributes（属性），也拥有Behaviours（行为），因此定义一个类来描述它是最好的方法；与此同时，在这个game object的基础上，还可以加上更多的属性与行为，使其成为另一个全新的对象，这里自然而然地会想到用继承来定义另一个新物体。\n但是很多时候，这两个物体之间并不是严格的父类与子类的关系（e.g.水路两栖车到底派生自车类还是船类？），所以，现代游戏引擎通常会使用Component Base（组件化）的方法来解决这一问题：将单个game object的许多不同部件拆分出来，作为单独的函数，在ComponentBase这个基类中定义基础的属性接口\n总结：1.在游戏世界中，所有物体都被抽象为Game Object\n2.所有的Game Objects又由许多的Component组成\n如何构建动态的世界：\n通过Tick（）实现\n游戏画面实际上是由大量连续帧组成的，以30fps为例，引擎每1/30秒就会调用一次Tick（）函数，对动画，物理等等进行更新，但这里的Tick（）并不是逐对象实现，而是逐组件实现的（e.g.先完成动画系统的更新，再去进行相机系统的更新）\nWhy：简单来说，就是现代计算机流水线的批处理效率比逐个处理单物体的效率要高\n如何创建交互：\n通过事件进行\n当对象发生某个行为时，周围的其他对象可能需要对这一行为作出反应，在早期的游戏引擎中，这一部分通常是Hard Code，即在类中定义所有对象对于这一行为的反应。\n但随着游戏对象的复杂化，硬编码的效率太低，有时甚至无法运行，于是出现了另一种方法：事件；对于每个对象，它并不需要知道周围其他对象应当作出何种反应，他只需要告诉对应的game object发生了什么事件，之后交由其他对象自行调用对应的方法并产生影响\n如何管理Game Objects：\n既然已经知道发生了某个事件，那应该如何寻找到受影响的物体？\n在小型游戏中，可以通过将地图划分为一个一个的小格子，以此来判断哪些区域（格子）内的物体会受到影响；\n但是游戏内的资源分布往往是不均匀的，如果平均划分的话会产生性能浪费，所以应当使用更加高效的场景管理策略，比如：BVH，八叉树等等\n更加复杂的情况：\n在很多情况下，消息的发送并不是单向的，而是会在多个系统之间双向进行，因此消息发送的顺序尤为重要；为了解决顺序的问题，引擎通常会将所有消息先收集起来，之后再按照一定的顺序进行发送（e.g.相当于邮局，对象将所有的信件都先寄邮局，之后再由邮局按照顺序统一发出）\n原文：如何构建游戏世界\n","permalink":"https://jiat-t.github.io/graphics/games104-game-world/","summary":"整理 Game Object 与组件化、Tick 更新、事件交互和场景空间管理的基本思路。","title":"GAMES104：如何构建游戏世界"},{"content":"资源层，平台层，工具层，功能层，核心层\u0026hellip;\u0026hellip;..\n资源层：将resource转化成assets，因为在其他DCC软件中（如：maya，3dmax)，他们的文件格式(.max，.maya\u0026hellip;.)往往是兼容自身软件，其中包含着大量的无用信息，因此，引擎需要将其转换成更加高效的\t类型(DDS)。\n而在Resource Layer中，每个资源都会有一个独特的GUID（Globally Unique Identifier，全局唯一标识符），勇于查找该对象并使用。即使文件的路径发生了改变，也可以通过GUID识别到需要使用的文件\n这一层也负责资产生命周期的管理，不同的资源有着不同的生命周期，而有限的内存要求我们及时释放不需要的资源，主要的方法有Garbage Collection（GC）和Deferred Loading\n功能层：物理，渲染，动画等等功能的实现\ne.g.Tick（）函数：分为TickLogic（）和TickRender（），分别处理逻辑与渲染计算，要求每帧更新\n多线程的发展趋势：在过去，CPU只有单核，物理，渲染等等的计算往往只会写在这一条线程上；随着硬件的发展，CPU的核心数量增加，也就使得多个线程可以并行计算（如：一个核心负责动画功能，一个核心则负责物理解算\u0026hellip;.）；但未来的引擎一定是采用的“Job System”，也就是将单个功能拆解为多个小的job，分散至不同的核心中进行计算，充分利用所有的性能，但是因为牵扯到多个系统的计算先后顺序问题，因此比较复杂\n核心层：顾名思义，引擎中最重要的部分，是整个引擎的基础。\n因为游戏引擎极度重视效率，像是C++的STL就不能使用（会产生内存空洞），因此我们需要在这一层自定义数据结构，以确保运行的高效性。同时也要求核心层的代码质量高，且不能轻易修改\n平台层：在不同的平台上（Windows，Mac\u0026hellip;.），文件路径，图形API，硬件架构等都可能有很大差异，因此引擎还需要对这些不同的平台进行不同的处理\n工具层：需要允许别人能够创建游戏内容，开发方式灵活，有时候代码量可能会比整个引擎还大\n外部的DCC工具（3DMAX，MAYA，Houdini等）也属于这一层\n为什么要进行分层？ 游戏引擎非常复杂，进行分层可以解耦与降低复杂度，\n如图，越向上越灵活，越往下越稳定\n一般来说，只允许上层调用下层的功能，而禁止下层调用上层的功能\n原文：引擎架构分层\n","permalink":"https://jiat-t.github.io/graphics/games104-engine-architecture/","summary":"记录资源层、功能层、核心层、平台层与工具层的职责，以及引擎分层和依赖方向。","title":"GAMES104：引擎架构分层"},{"content":"函数***************\nTraceRay（）函数 ：\n作为启动RT Cores的指令，根据之前填写好的payload，在BVH中进行包围盒与三角形的相交测试\n在调用时，需要给出八个参数，e.g :\nTraceRay\n(\nSceneBVH, // 1. RaytracingAccelerationStructure: 整个 3D 世界的加速结构\nRAY_FLAG_NONE, // 2. RayFlags: 光线行为标志位\n0xFF, // 3. InstanceInclusionMask: 剔除掩码 (0xFF 表示与所有物体发生交互，不忽略任何人)\n0, // 4. RayContributionToHitGroupIndex: SBT 命中组的基址偏移 (决定调用哪个 ClosestHit)\n1, // 5. MultiplierForGeometryContributionToHitGroupIndex: SBT 几何体步进乘数\n0, // 6. MissShaderIndex: 如果啥都没撞到，调用 SBT 里的第几个 Miss 着色器\nRay, // 7. RayDesc: 光线的起点、方向、长短\npayload\t// 8. Payload: 随光线传递的背包\n);\nSetMaxRecursionDepth（）函数 ：\n在CreateRaytracingPipeline（）中被调用，提前告诉GPU这条光线会弹射几次（在括号中填充数值）\nOMSetStencilRef（）函数 ：\n全称是 ID3D12GraphicsCommandList::OMSetStencilRef\n可以拆解为：Output Merger SetStencilReference，它用于在渲染管线的最后一个阶段（输出合并阶段）设置模板测试（Stencil Test）的参考值。\nXMMatrixShadow（）函数 ：\n用于构建在特定平面内投影所用的阴影矩阵，定义如下：\ninline XMMATRIX XM_CALLCONV XMMatrixShadow(\nFXMVECTOR ShadowPlane,\nFXMVECTOR LightPosition);\n原文：函数\n","permalink":"https://jiat-t.github.io/posts/dxr-dx12-functions/","summary":"整理 TraceRay 的参数、光线递归深度设置和 OMSetStencilRef 等函数的用途。","title":"DXR / DX12 常用函数"},{"content":"类型***************\nIDxcBlob :\nBlob的全名 ： Binary Large Object（二进制大对象）\nIDxcBlob 属于 CPU 准备数据 这一阶段的产物 ， 本质上就是对一块内存的封装\n作为 CPU（餐厅经理），你写了一份极为复杂的后厨操作指南（HLSL Shader 代码）。你把这份人类可读的指南送到了翻译部门（DirectX Shader Compiler, 简称 DXC），DXC 将其翻译成 GPU 能看懂的机器码（DXIL - DirectX Intermediate Language），然后把它塞进一个 IDxcBlob 这个“图纸管”里交还给你。\nXMVECTOR ：\n一个四维向量\n原文：类型\n","permalink":"https://jiat-t.github.io/posts/dxr-dx12-types/","summary":"记录 IDxcBlob、DXC 编译产物和 XMVECTOR 等 DirectX 开发中的常用类型。","title":"DXR / DX12 常用类型"},{"content":"作业4是使用 De Casteljau 算法实现 Bézier 曲线\nDe Casteljau 算法的核心是递归，使用递归，通过输入的多个顶点计算出连续插值后的最后一个顶点\n核心函数：\nrecursive_bezier（）\n1cv::Point2f recursive_bezier(const std::vector\u0026lt;cv::Point2f\u0026gt; \u0026amp;control_points, float t) 2{ 3 // TODO: Implement de Casteljau\u0026#39;s algorithm 4 if (control_points.size() == 1) return control_points.front(); 5 6 std::vector\u0026lt;cv::Point2f\u0026gt; next_points; 7 for (int i = 0; i \u0026lt; control_points.size() - 1; i++) 8 { 9 next_points.push_back(((1 - t) * control_points[i] + t * control_points[i + 1])); 10 } 11 12 return recursive_bezier(next_points, t); 13} 在循环中计算当前顶点与下一顶点的插值点，并将其push_back进要在下一层递归中使用的顶点组；每次递归都会使总顶点数减一，直到只剩最后一个顶点，将其作为返回值输出\nbezier（）\n1void bezier(const std::vector\u0026lt;cv::Point2f\u0026gt; \u0026amp;control_points, cv::Mat \u0026amp;window) 2{ 3 // TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau\u0026#39;s 4 // recursive Bezier algorithm. 5 for (double t = 0; t \u0026lt;= 1; t += 0.0001) 6 { 7 auto point = recursive_bezier(control_points, t); 8 window.at\u0026lt;cv::Vec3b\u0026gt;(point.y, point.x)[1] = 255; 9 } 10} 在这个函数中，通过对t值进行细微的调整，可以得到平滑的曲线效果（本质就是在两个点之间计算出大量的插值点）\nExtra：为曲线添加反走样\n我们在循环中所计算的坐标是有零1有整的，比如（10.2，15.8），但是程序会把它转换为距离最近的整数（10，16），这也是锯齿产生的原因\nbezier（）\n1void bezier(const std::vector\u0026lt;cv::Point2f\u0026gt; \u0026amp;control_points, cv::Mat \u0026amp;window) 2{ 3 // TODO: Iterate through all t = 0 to t = 1 with small steps, and call de Casteljau\u0026#39;s 4 // recursive Bezier algorithm. 5 6 for (double t = 0; t \u0026lt;= 1; t += 0.0001) 7 { 8 int color = 0; 9 auto point = recursive_bezier(control_points, t); 10 int x = std::floor(point.x); 11 int y = std::floor(point.y); 12 13 std::vector\u0026lt;cv::Point2f\u0026gt; near_pixels 14 { 15 cv::Point2f(x + 1, y + 1), cv::Point2f(x + 1, y), 16 cv::Point2f(x, y + 1), cv::Point2f(x, y) 17 }; 18 for (const auto\u0026amp; near : near_pixels) 19 { 20 int nx = static_cast\u0026lt;int\u0026gt;(near.x); 21 int ny = static_cast\u0026lt;int\u0026gt;(near.y); 22 23 if (nx \u0026lt; 0 || nx \u0026gt;= window.cols || ny \u0026lt; 0 || ny \u0026gt;= window.rows) continue; 24 25 float distance = std::sqrt((point.x - near.x) * (point.x - near.x) + 26 (point.y - near.y) * (point.y - near.y)); 27 28 float weight = 1.0f - (distance / 1.414f); 29 int new_green = std::max(0, static_cast\u0026lt;int\u0026gt;(weight * 255.0f)); 30 31 auto\u0026amp; pixel = window.at\u0026lt;cv::Vec3b\u0026gt;(ny, nx); 32 pixel[1] = std::max(static_cast\u0026lt;int\u0026gt;(pixel[1]), new_green); 33 } 34 } 35} 有两种方法，一种是通过周围的点计算当前的颜色值，但是在这个循环中，这个方法有些麻烦\n所以，另一种方法是使用当前点的颜色值，通过欧氏距离插值，赋予周围的点颜色\n流程：\n1.先对当前坐标取整，并计算包围着当前的的四个点（2x2采样）\n2.通过遍历，分别计算distance并赋予颜色值（取最大值）\n原文：4\n","permalink":"https://jiat-t.github.io/graphics/games101-assignment-4/","summary":"记录 De Casteljau 递归插值绘制 Bézier 曲线的方法，以及采样步长与曲线绘制代码。","title":"GAMES101 作业 4：Bézier 曲线"},{"content":"作业2 是实现三角形的光栅化 ：\n需要修改的函数如下：\n• rasterize_triangle(): 执行三角形栅格化算法\n• static bool insideTriangle(): 测试点是否在三角形内\n首先判断点是否在三角形内部\n通过计算重心坐标，得到alpha，beta，gamma的值，将其与零进行比较，如果其中存在一个值小于零，那么就可以判断此点不在三角形之内，之后就只需要对内部的点进行深度测试与颜色赋值\n数学原理：重心坐标计算出来的三个值实际上是任意一点对三角形的三个顶点的权重，而位于三角形内的点，其权重必然大于等于零\ninsideTriangle（）\n1static bool insideTriangle(float x, float y, const Vector3f* _v) 2{ 3 // TODO : Implement this function to check if the point (x, y) is inside the triangle represented by _v[0], _v[1], _v[2] 4 5 auto bar = computeBarycentric2D(x, y, _v); 6 if (std::get\u0026lt;0\u0026gt;(bar) \u0026lt; 0 || std::get\u0026lt;1\u0026gt;(bar) \u0026lt; 0 || std::get\u0026lt;2\u0026gt;(bar) \u0026lt; 0) 7 return false; 8 return true; 9} 光栅化在rasterize_triangle()函数内进行，在这个函数中需要执行是否在内部的判断以及深度测试（extra：双重采样）\n这里使用的是包围盒算法——找出恰好能够将三角形包围在内的包围盒，之后就只需要在这和盒子内进行每一个像素点的遍历，而不是遍历整个画面\nrasterize_triangle（）\n1void rst::rasterizer::rasterize_triangle(const Triangle\u0026amp; t) { 2 auto v = t.toVector4(); 3 4 // TODO : Find out the bounding box of current triangle. 5 // iterate through the pixel and find if the current pixel is inside the triangle 6 7 // Create Bounding Box 8 float X_max = (std::max(std::max(v[0].x(), v[1].x()), v[2].x())); 9 float X_min = (std::min(std::min(v[0].x(), v[1].x()), v[2].x())); 10 float Y_max = (std::max(std::max(v[0].y(), v[1].y()), v[2].y())); 11 float Y_min = (std::min(std::min(v[0].y(), v[1].y()), v[2].y())); 12 13 for (int x = X_min; x \u0026lt;= X_max; x++) 14 { 15 for (int y = Y_min; y \u0026lt; Y_max; y++) 16 { 17 if (insideTriangle(x, y, t.v)) 18 { 19 auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v); 20 float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w()); 21 float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w(); 22 z_interpolated *= w_reciprocal; 23 24 if (z_interpolated \u0026lt; depth_buf[get_index(x, y)]) 25 { 26 depth_buf[get_index(x, y)] = z_interpolated; 27 Vector3f point = { (float)x,(float)y, z_interpolated }, color = t.getColor(); 28 set_pixel(point, color); 29 } 30 } 31 } 32 } 33} 原文：2\n","permalink":"https://jiat-t.github.io/graphics/games101-assignment-2/","summary":"记录三角形内部判断、包围盒遍历、重心坐标与深度测试的光栅化实现。","title":"GAMES101 作业 2：三角形光栅化"},{"content":"DXR的五种着色器\n关于TLAS和BLAS：\n在 DXR中，我们不再是简单地把顶点丢给管线去画三角形，而是要在三维空间中发射数百万条光线，并计算它们与场景的交点。这是一个极其恐怖的计算量。为了让顶级显卡能在几毫秒内算完，我们必须建立一种极其高效的空间数据结构\n一种解决方法是，创建包围盒（BV），此时光线只需要对三个对面的交集求交，如果与BV存在交集，则进一步对包含在其中的物体进行求交；反之，如果光线与更大的BV都没有交集，就更不可能与位于其中的物体存在交集。\n但当场景内的物体变多时，这样的计算速度仍然很低，于是可以采用层次包围盒的方法（BVH），即在多个包围盒形成的集合之外再镶套包围盒，检测原理同上。\n但但但但但是，整个场景中只存在一个BVH还是不够，因为这意味着如果场景中的一个物体出现了移动（哪怕只有1mm），整个场景都需要重新计算光照（光线弹射），因此将BVH又分层为TLAS（Top-Level Acceleration Structure）和BLAS（ Bottom-Level Acceleration Structure）。\n其中，BLAS是单个物体在Local Space的BVH，TLAS实质是树，其子节点包含世界变换矩阵与一个指向特定BLAS显存地址的指针 |\n用于将光线变换至BLAS所在的Local Sapce\n构建RTPSO时的三个核心成员\n关于Desc的一些理解：\n在现代图形架构下，DESC（如 D3D12_RESOURCE_DESC）的本质是 CPU 向 GPU 驱动提交的一份“具备法律效力的显存施工图纸”。它的作用可以总结为三个维度：\n跨界通信的契约 (Bridge the Async Gap): 正如你所理解的，GPU 是无法凭空去 CPU 内存里抓取数据的。CPU 是下订单的餐厅经理，GPU 是后厨流水线。DESC 就是经理写在点菜单上的详细规格（要几斤肉、切多厚）。这纯粹是 CPU 端准备数据的过程，在调用分配 API 之前，GPU 完全不知道它的存在。 精准圈地与物理对齐 (Memory Carving): GPU 显存（VRAM）是高度碎片化且对齐要求极苛刻的。DESC 里的宽高、格式、Mip层数，让驱动程序能够精确计算出需要划拨多少字节的物理显存，并安排最优的底层存储布局（Swizzle Pattern）。 权限与管线绑定声明 (Capability Locking): 我们说过，Root Signature (根签名) 是 C++ 函数的参数列表。那么，DESC 里的 Flags 字段，就决定了这个资源未来能不能作为实参传给特定的函数！如果你没在 DESC 里声明 ALLOW_RENDER_TARGET，哪怕这块内存再大，它也永远不能挂载到渲染管线的输出端。 CBV，SRV，UAV等等都是什么东西：\nSBT（Shader Binding Stable）：\n不同于光栅化，在DXR中，CPU无法预知光线会打到哪里，因此，现在我的理解就是，\nSBT的作用是：因为CPU无法预知光线会打在哪个材质的表面，因此需要提前写好SBT，用于所击中的片元的渲染，比如当光线击中玻璃材质时，就调用CHS和AHS，当什么也没有击中时，调用MS；\n也就是说，现在渲染的主体是一束光线，我们跟着光线的路径去渲染片元，而不是像光栅化的时候那样，逐物体的去进行渲染\nRayGen着色器的payload：\nPayload 是一个完全驻留在 GPU 极速寄存器中的‘轻量级通信穿梭机’。它只负责在 DXR 异步遍历加速结构（BVH）时，收集并带回最核心的‘交点上下文（如实例 ID 和重心坐标）’或‘布尔状态（如阴影遮挡）’，从而将高延迟的‘显存读取（采样纹理、读取法线）’和‘重度光照计算’推迟并集中到 RayGen 着色器中执行，以保证 GPU 拥有最高的并行吞吐率（Occupancy）。 HitGroup的组成与作用：\nVector的data（）函数的作用：\n.data() 的作用：剥开包装，暴露本质 如果你直接把 matrices 传给 memcpy，你拷贝的只是 std::vector 内部的那三个管理指针，传到 GPU 里全是垃圾数据，直接导致画面崩溃。\n.data() 方法的作用，就是向 std::vector 索要它内部那块连续内存的起始原生指针 (Raw Pointer)。\n比喻：std::vector 就像是一艘高度现代化的集装箱货轮（有船长、航海日志、容量信息）。memcpy 是码头上的巨型起重机，它只负责吊装里面的货物（数据）。.data() 就是打开货舱盖，直接把第一个集装箱的挂钩交到起重机手里。 Register的四个槽位：\nb: 体积小、更新频率高、所有线程共享的只读常量数据 t: 只读的内存资源 (DXR的AS也在这个槽位）\ns: 采样器 u: 允许 GPU 随意读写的资源 基础：根签名与描述符：\n根签名——相当于一张说明书，告诉了GPU每个槽位应该装些什么（slot1，slot2\u0026hellip;.)，它本身不包含任何资源\n描述符——对送往GPU的资源进行描述的轻量级结构，既包含实际的资源，同时也包含如何使用该资源的描述\n原文：DXR\n","permalink":"https://jiat-t.github.io/posts/directx-raytracing-notes/","summary":"记录 DXR 着色器、TLAS 与 BLAS、光线追踪管线，以及资源描述结构在 CPU 与 GPU 之间的作用。","title":"DXR 基础笔记"},{"content":"\n核心概念：什么是 HRESULT？ (The Why) 从技术上讲，HRESULT 是一个 32位整数（long），它是 COM (Component Object Model) 组件的标准返回值。DirectX 是基于 COM 构建的，所以沿用了这一标准。\nHRESULT 是与图形硬件沟通的第一道防线，它是 API 调用的“心跳报告”。\n在 D3D12 中，几乎所有的 API 调用（创建设备、分配内存、编译着色器）都会返回一个 HRESULT。如果你忽略它，你就是在蒙眼狂奔。\nHRESULT 是一个变量类型（C++中的 long），它是 Windows 编程中用来记录函数执行结果（包括错误类型、成功状态）的标准载体。\n在 DirectX 12 和 Windows COM 编程中，它不仅记录“出错了”，还记录“错哪了”甚至“成功了但有些小状况”。\n在 C++ 的头文件定义中，它其实就是一个 32 位的整数： typedef long HRESULT;所以，当你看到 HRESULT hr = \u0026hellip; 时，你本质上是在存一个数字。 但是，这个数字并不是随机生成的，它像一个“比特位地图”。\n2. 它的内部结构（为什么它是 32 位的？） 虽然它只是一个整数，但它的 32 个二进制位（bit）被划分成了三个特定的区域。这就好比身份证号码，每一段数字都有特定含义。\n我们通常用 16 进制来看它，比如常见的失败代码 0x80070057：\n第 31 位 (Severity - 严重性)：最重要的一位！ 0 = 成功 (Success)1 = 失败 (Fail)这就是为什么所有的错误代码（如 0x8...）开头都是 8（二进制 1000...），因为第 31 位是 1。 第 16-30 位 (Facility - 来源)：表示是谁报错的。 比如 0x007 代表 Win32 API，0x87A 代表 DXGI (DirectX Graphics Infrastructure)。 第 0-15 位 (Code - 具体代码)：具体的错误原因。 比如 5 代表 \"Access Denied\" (拒绝访问)，57 代表 \"Invalid Argument\" (参数无效)。 throw的含义：\nGUID ： 它本质上是一个 128位 (16字节) 的超大整数， 是每一个组件的“身份证”\nIID_PPV_ARGS ：\n实现代码：#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), (void**)(ppType)\n1.自动获取GUID，防止自己手误填错；\n/*假设你传入的是 \u0026amp;device：\n1）device 的类型是 ID3D12Device*（指向设备的指针）。\n2）ppType (也就是 \u0026amp;device) 的类型是 ID3D12Device**（指向指针的指针）。\n3）*(ppType) 解引用一次，变成了 ID3D12Device*。\n4）**(ppType) 再解引用一次，变成了 ID3D12Device (类型本身)。\n结论：__uuidof 直接去查“你传入的这个变量所属的类型”的身份证号。 如果你传的是 device，它就自动填 ID3D12Device 的 GUID。你根本没有机会填错*/\n2.进行类型转换，返回指向函数的指针\nFence:\n实质： Fence 本质上就是一个所有的 CPU 和 GPU 都能访问的 64位整数 (UINT64)\ne.g. 1.\n2.\nps：这个方法是强制刷新命令队列，cpu会一直对单帧的绘制进行等待；后面用到了其他的方法，叫做双缓冲或三缓冲，意思是在提交了第一帧的命令之后，CPU不会傻等，而是去填写第二帧与第三帧命令，此时只有当第一帧还没有绘制完时，CPU才会等待\n获取描述符大小：\n作用：\n设置MSAA抗锯齿属性：\nflag：\nD3D12 的硬件兼容性检查非常严格。\n你不能直接问“支持 MSAA 吗？” 你必须精确地问：“对于 R8G8B8A8 这种颜色格式，做 4倍 采样，你支不支持？” 如果你换了颜色格式（比如 DXGI_FORMAT_R16G16B16A16_FLOAT HDR格式），你得重新问一次，因为显卡可能支持普通颜色的 MSAA，但不支持 HDR 颜色的 MSAA。 命令队列和命令列表\nCPU 使用命令列表（List）作为工具，将指令数据写死在分配器（Allocator）提供的内存上，最后将整块内存的引用交给队列（Queue）去执行\n交换链：\n交换链本质就是观众在看A黑板而这时候你在写B黑板，之后在把B黑板调换位置，观众永远能看见完整画面\nCD3DX12_CLEAR_VALUE optClear：\n资源的转换：\n关于为什么每次使用comptr类型变量都要调用Get（）方法：\n窗口的初始化：\n在 Windows 中，你不能直接说“给我个窗口”。你必须先填写一张详细的“申请表”，定义这个窗口的行为和长相：WNDCLASS wc;\n为什么rtv和dsv要用句柄访问而不是直接使用堆上的数据：\nD3D12_RESOURCE_STATE_PRESENT 是 CPU、GPU 渲染核心、显示控制器三者之间的一个契约。\n它保证了当屏幕读取数据时，数据是完整的、可读的且不再被修改的。如果你不切换到这个状态直接 Present，Debug Layer 会直接报错，而在真机上，你可能会看到花屏、闪烁或者显卡驱动崩溃。\n深度/模板测试：\nPart1 ：\n实现镜面效果\u0026mdash;\u0026mdash;\n一种方法是模板测试，但这种方法仅局限于不产生形变的平面镜。\n首先沿着镜面的对称轴（镜面Local Space的x轴（应该是吧？））再创建一个一样的模型，接着根据对称轴创建镜像矩阵，将光照方向也镜像，这个光照将用于镜像物体的渲染\n之后我们需要在RenderItem结构体中添加layer成员变量，用于控制渲染过程使用的不同的passconstant ： 在BuildRenderItems（）中赋予属性，在Draw（）中进行调用\n原文：DX12\n","permalink":"https://jiat-t.github.io/posts/directx12-hresult/","summary":"整理 HRESULT 的返回值含义、位字段和成功与失败判断，以及 DirectX 12 调用中的错误检查方式。","title":"DX12：HRESULT 与错误检查"},{"content":"","permalink":"https://jiat-t.github.io/posts/my_shared_ptr/","summary":"","title":""},{"content":"","permalink":"https://jiat-t.github.io/posts/normal-combination/normal-combination/","summary":"","title":""},{"content":"","permalink":"https://jiat-t.github.io/posts/plane-shadow-matrix-derivation/plane-shadow-matrix-derivation/","summary":"","title":""},{"content":"这里优先展示渲染与引擎方向的六个项目。代码、现有预览、运行依赖与当前限制见各仓库 README；性能数字应结合原始测试条件阅读。\nCPU 渲染与 C++ PathTracer-CPP：GGX/VNDF、灯光与环境重要性采样的 MIS、BVH、并行 Tile、渐进累积与 A-Trous 降噪。适合从光传输、采样和加速结构三个角度阅读。\nSoft-Raster-Renderer：小型软件光栅化器，展示三角形插值、深度测试、Shadow Map、TBN 与 SSAO。原始示例模型未完整提交，需准备有使用权限的资源。\n实时图形 API DX12-Renderer：Win32/DirectX 12 的资源与渲染流程，HLSL PBR、IBL、阴影 PCF、HDR 和 ACES。外部模型与 HDRI 的许可及获取方式需要独立确认。\nVulkan-Renderer：前向、延迟与离屏渲染实验，以及深度/G-buffer 可视化、透明混合和 SDR/HDR 输出。重点是资源、Render Pass、同步与输出路径的显式管理。\nUnreal Engine UE5-FFT-Ocean：基于 Epic 社区 FFT 海面教程，使用 Niagara GPU Simulation Stage 和 USH/HLSL。仓库保留教程来源，包含海面材质、泡沫、散射与级联相关迭代；教程基础结构与个人调试工作应分别理解。\nUE5-Procedural-Grassland：通过 PCG 与材质资产组织草地生成、WPO、实例数据、LOD 和交互 Render Target。主要实现位于 UE 资产图中，源码中的模板角色代码不能代替对 PCG/材质图的检查。\n继续阅读 图形学笔记 · Vulkan 笔记 · Unreal Engine 笔记 · C++ 笔记 · 完整笔记索引\n","permalink":"https://jiat-t.github.io/portfolio/","summary":"\u003cp\u003e这里优先展示渲染与引擎方向的六个项目。代码、现有预览、运行依赖与当前限制见各仓库 README；性能数字应结合原始测试条件阅读。\u003c/p\u003e\n\u003ch2 id=\"cpu-渲染与-c\"\u003eCPU 渲染与 C++\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/JiaT-T/PathTracer-CPP\"\u003ePathTracer-CPP\u003c/a\u003e：GGX/VNDF、灯光与环境重要性采样的 MIS、BVH、并行 Tile、渐进累积与 A-Trous 降噪。适合从光传输、采样和加速结构三个角度阅读。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/JiaT-T/Soft-Raster-Renderer\"\u003eSoft-Raster-Renderer\u003c/a\u003e：小型软件光栅化器，展示三角形插值、深度测试、Shadow Map、TBN 与 SSAO。原始示例模型未完整提交，需准备有使用权限的资源。\u003c/p\u003e\n\u003ch2 id=\"实时图形-api\"\u003e实时图形 API\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/JiaT-T/DX12-Renderer\"\u003eDX12-Renderer\u003c/a\u003e：Win32/DirectX 12 的资源与渲染流程，HLSL PBR、IBL、阴影 PCF、HDR 和 ACES。外部模型与 HDRI 的许可及获取方式需要独立确认。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/JiaT-T/Vulkan-Renderer\"\u003eVulkan-Renderer\u003c/a\u003e：前向、延迟与离屏渲染实验，以及深度/G-buffer 可视化、透明混合和 SDR/HDR 输出。重点是资源、Render Pass、同步与输出路径的显式管理。\u003c/p\u003e\n\u003ch2 id=\"unreal-engine\"\u003eUnreal Engine\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/JiaT-T/UE5-FFT-Ocean\"\u003eUE5-FFT-Ocean\u003c/a\u003e：基于 Epic 社区 FFT 海面教程，使用 Niagara GPU Simulation Stage 和 USH/HLSL。仓库保留教程来源，包含海面材质、泡沫、散射与级联相关迭代；教程基础结构与个人调试工作应分别理解。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/JiaT-T/UE5-Procedural-Grassland\"\u003eUE5-Procedural-Grassland\u003c/a\u003e：通过 PCG 与材质资产组织草地生成、WPO、实例数据、LOD 和交互 Render Target。主要实现位于 UE 资产图中，源码中的模板角色代码不能代替对 PCG/材质图的检查。\u003c/p\u003e\n\u003ch2 id=\"继续阅读\"\u003e继续阅读\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"/graphics/\"\u003e图形学笔记\u003c/a\u003e · \u003ca href=\"/vulkan/\"\u003eVulkan 笔记\u003c/a\u003e · \u003ca href=\"/ue/\"\u003eUnreal Engine 笔记\u003c/a\u003e · \u003ca href=\"/cpp/\"\u003eC++ 笔记\u003c/a\u003e · \u003ca href=\"/archives/\"\u003e完整笔记索引\u003c/a\u003e\u003c/p\u003e","title":"渲染与引擎项目"}]