1.简介

本来按照计划这一系列的文章应该介绍Context和Page两个内容的,但是宏哥看了官方文档和查找资料发现其实和宏哥在Python+Playwright系列文章中的大同小异,差不了多少,再在这一个系列介绍就有点画蛇添足,索性就不介绍和讲解了,有兴趣的自己可以看宏哥之前写的,或者自己查找资料和官方文档进行了解和学习。今天讲解和分享的标签操作其实也是基于浏览器上下文(
BrowserContext
)进行操作的,而且宏哥在之前的BrowserContext也有提到过,但是有的童鞋或者小伙伴还是不清楚怎么操作,或者思路有点模糊,因此今天单独来对其进行讲解和分享一下,希望您有所帮助。

2.什么是tab标签页

Tabs 标签页又称选项卡(以下简称标签页),它是一种高效的屏幕空间利用手段,映射非常接近卡片的目录索引,用户可以基于索引标签,快速定位到目标中内容中去,这也是大多数用户来自现实世界的经验。

在 Web 页面中,它的使用场景也较为简单,当页面的内容信息量较多,用标签页可以对其分类,一方面可以提升查找信息的效率,另一方面可以精简用户单次获取到的信息量,用户更能够专注于当前已显示的内容。

‌标签页(Tab)是一种用户界面元素,用于组织和管理网页或应用程序中的内容,允许用户在不同的视图或数据集之间轻松切换。‌

标签页的设计灵感来源于现实生活中文件夹上的标签,通过隐喻的方式,设计师希望用户能够通过直观的方式理解和使用这一交互形式。

3.单标签页

单个标签操作这个是最简单的,之前讲的绝大多数都是单个标签的操作。通过context.new_page()就可以创建一个页面。

每个BrowserContext可以有多个页面。页面是指浏览器上下文中的单个选项卡或弹出窗口。它应该用于导航到URL并与页面内容交互。

//Create a page.
Page page =context.newPage();//Navigate explicitly, similar to entering a URL in the browser.
page.navigate("http://example.com");//Fill an input.
page.locator("#search").fill("query");//Navigate implicitly by clicking a link.
page.locator("#submit").click();//Expect a new url.
System.out.println(page.url());

实战举例:以度娘为例,首先启动浏览器,然后再设置浏览器的大小。查询“北京宏哥”后,刷新页面后执行回退操作到百度首页,然后有执行前进操作进入到搜索“北京宏哥”页面,最后退出浏览器。

3.1代码设计

按照上边的步骤进行代码设计,如下图所示:

3.2参考代码

packagecom.bjhg.playwright;importcom.microsoft.playwright.Browser;importcom.microsoft.playwright.BrowserContext;importcom.microsoft.playwright.BrowserType;importcom.microsoft.playwright.Locator;importcom.microsoft.playwright.Page;importcom.microsoft.playwright.Playwright;/***@author北京-宏哥
*
* @公众号:北京宏哥(微信搜索,关注宏哥,提前解锁更多测试干货)
*
* 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-10- 标签页(tab)操作 (详细教程)
*
* 2024年8月26日
*/ public classTest_Search {public static voidmain(String[] args) {try (Playwright playwright =Playwright.create()) {//1.使用chromium浏览器,# 浏览器配置,设置以GUI模式启动Chrome浏览器(要查看浏览器UI,在启动浏览器时传递 headless=false 标志。您还可以使用 slowMo 来减慢执行速度。 Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setSlowMo(500));//2.设置浏览器窗口大小 BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1280, 1024));//创建page Page page =context.newPage();//3.浏览器打开百度 page.navigate("https://www.baidu.com/");//判断title是不是 百度一下,你就知道 try{
String baidu_title
= "百度一下,你就知道";assert baidu_title ==page.title();
System.out.println(
"Test Pass");

}
catch(Exception e){
e.printStackTrace();
}
//使用xpath属性定位百度首页输入框 ,并输入搜索内容:北京-宏哥 page.locator("//*[@id='kw']").type("北京-宏哥");//使用路径与属性结合定位“百度一下”按钮,并点击 。 page.locator("//span/input[@id='su']").click();//5.刷新页面 page.reload();//6.浏览器后退 page.goBack();//7.浏览器前进 page.goForward();//关闭page page.close();//关闭browser browser.close();
}
}

}

3.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作。如下图所示:

4.多标签页

每个浏览器上下文可以承载多个页面(选项卡)。

  • 每个页面都像一个聚焦的活动页面。不需要将页面置于最前面。
  • 上下文中的页面遵循上下文级别的模拟,例如视口大小、自定义网络路由或浏览器区域设置。

// Create two pages
Page pageOne = context.newPage();
Page pageTwo = context.newPage();

// Get pages of a browser context
List<Page> allPages = context.pages();

实战举例:在page_one 标签页打开百度,输入“北京-宏哥”, 在page_two 标签页打开百度,输入“宏哥”。

4.1代码设计

按照上边的步骤进行代码设计,如下图所示:

4.2参考代码

packagecom.bjhg.playwright;importcom.microsoft.playwright.Browser;importcom.microsoft.playwright.BrowserContext;importcom.microsoft.playwright.BrowserType;importcom.microsoft.playwright.Locator;importcom.microsoft.playwright.Page;importcom.microsoft.playwright.Playwright;/***@author北京-宏哥
*
* @公众号:北京宏哥(微信搜索,关注宏哥,提前解锁更多测试干货)
*
* 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-10- 标签页(tab)操作 (详细教程)
*
* 2024年8月26日
*/ public classTest_Search {public static voidmain(String[] args) {try (Playwright playwright =Playwright.create()) {//1.使用chromium浏览器,# 浏览器配置,设置以GUI模式启动Chrome浏览器(要查看浏览器UI,在启动浏览器时传递 headless=false 标志。您还可以使用 slowMo 来减慢执行速度。 Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false).setSlowMo(500));//2.设置浏览器窗口大小 BrowserContext context = browser.newContext(new Browser.NewContextOptions().setViewportSize(1280, 1024));//创建page Page page1 =context.newPage();//3.浏览器打开百度 page1.navigate("https://www.baidu.com/");//使用xpath属性定位百度首页输入框 ,并输入搜索内容:北京-宏哥 page1.locator("//*[@id='kw']").type("北京-宏哥");

Page page2
=context.newPage();//3.浏览器打开百度 page2.navigate("https://www.baidu.com/");//使用xpath属性定位百度首页输入框 ,并输入搜索内容:北京-宏哥 page2.locator("//*[@id='kw']").type("宏哥");//关闭page page1.close();
page2.close();
//关闭browser browser.close();
}
}

}

4.3运行代码

1.运行代码,右键Run'Test',就可以看到控制台输出,如下图所示:

2.运行代码后电脑端的浏览器的动作。如下图所示:

5.小结

好了,关于标签页(tab)的相关操作非常简单,时间不早了今天就分享到这里,感谢你耐心地阅读!

AIModelRouter

AI模型路由,模型的能力有大小之分,有些简单任务,能力小一点的模型也能很好地完成,而有些比较难的或者希望模型做得更好的,则可以选择能力强的模型。为什么要这样做呢?可以降低AI模型的使用成本,毕竟能力强的模型会更贵一点,省着用挺好的。

Semantic Kernel中可以很简便地使用一个AIModelRouter。

实践

先来一个简单的例子

来自https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/Demos/AIModelRouter

新建一个CustomRouter类,如下所示:

internal sealed class CustomRouter()
{
    internal string GetService(string lookupPrompt, List<string> serviceIds)
    {
        // The order matters, if the keyword is not found, the first one is used.
        foreach (var serviceId in serviceIds)
        {
            if (Contains(lookupPrompt, serviceId))
            {
                return serviceId;
            }
        }

        return serviceIds[0];
    }

    // Ensure compatibility with both netstandard2.0 and net8.0 by using IndexOf instead of Contains
    private static bool Contains(string prompt, string pattern)
        => prompt.IndexOf(pattern, StringComparison.CurrentCultureIgnoreCase) >= 0;
}

新建一个SelectedServiceFilter类用于打印一些信息:

 internal sealed class SelectedServiceFilter : IPromptRenderFilter
 {
     /// <inheritdoc/>
     public Task OnPromptRenderAsync(PromptRenderContext context, Func<PromptRenderContext, Task> next)
     {
         Console.ForegroundColor = ConsoleColor.Yellow;
         Console.WriteLine($"Selected service id: '{context.Arguments.ExecutionSettings?.FirstOrDefault().Key}'");

         Console.ForegroundColor = ConsoleColor.White;
         Console.Write("Assistant > ");
         return next(context);
     }
 }

使用多个模型:

image-20250106101815911

为捕获路由器选择的服务 ID 添加自定义过滤器:

image-20250106101942229

开启一个聊天循环:

        Console.ForegroundColor = ConsoleColor.White;

        ChatHistory history = [];
        string history1 = string.Empty;
        bool isComplete = false;

        do
        {
            Console.WriteLine();
            Console.Write("> ");
            string? input = Console.ReadLine();
            if (string.IsNullOrWhiteSpace(input))
            {
                continue;
            }
            if (input.Trim().Equals("EXIT", StringComparison.OrdinalIgnoreCase))
            {
                isComplete = true;
                break;
            }
            if (input.Trim().Equals("Clear", StringComparison.OrdinalIgnoreCase))
            {
                history.Clear();
                history1 = " ";
                Console.WriteLine("已清除聊天记录");
                continue;
            }

            history.Add(new ChatMessageContent(AuthorRole.User, input));
            history1 += $"User:{input}\n";

            Console.WriteLine();

            // Find the best service to use based on the user's input
            KernelArguments arguments = new(new PromptExecutionSettings()
            {
                ServiceId = router.GetService(input, serviceIds).Result,
                FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
            });

            // Invoke the prompt and print the response
            //await foreach (var chatChunk in kernel.InvokePromptStreamingAsync(userMessage, arguments).ConfigureAwait(false))
            //{
            //    Console.Write(chatChunk);
            //}
           
            var result = await kernel.InvokePromptAsync(history1, arguments).ConfigureAwait(false);
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine(result);
            Console.WriteLine();

            // Add the message from the agent to the chat history
            history.AddMessage(AuthorRole.Assistant, result.ToString());
            history1 += $"Assistant:{result}\n";
        } while (!isComplete);
    }
}

来看看现在这个简单的路由规则:

image-20250106102824888

当你的提问中包含一个ServiceId的时候,就会选择那个服务ID对应的模型进行回复,如果不包含就选择第一个服务ID对应的模型进行回复。

实际上这样使用,很容易让AI迷惑,因为我们总是要带上一个ServiceId,如果让AI根据用户的提问,自己决定用哪个模型是更好的。

进阶使用,用AI自己来决定

image-20250106103343454

使用一个靠谱的AI模型来做这个事情比较好。

我们输入你好,那么Prompt就会变成这样:

image-20250106103624167

AI返回的结果如下:

image-20250106103713305

image-20250106103742224

再试试其他几个怎么触发:

image-20250106103848889

而工具调用与其他比较容易混淆,因为就算是我们自己,也很难分辨有什么区别:

image-20250106104310185

这时候或许修改Prompt可以奏效。

修改后的Prompt如下:

 string skPrompt = """
          根据用户的输入,返回最佳服务ID。
          如果用户需要获取当前时间与写邮件,则选择工具调用相关的服务ID。
          用户输入:
          {{$input}}
          服务ID列表:
          {{$serviceIds}}
          无需返回任何其他内容,只需返回服务ID。              
     """;

效果如下所示:

image-20250106113558077

以上就是本次分享的全部内容,希望对你有所帮助。

1、下载gitlab

首先在
/etc/yum.repos.d/
目录下配置
gitlab
下载镜像源。

# 进入目录 /etc/yum.repos.d/
cd /etc/yum.repos.d/

# 创建文件 gitlab-ce.repo 
vim gitlab-ce.repo

# 添加以下内容
[gitlab-ce]
name=gitlab-ce
baseurl=https://mirror.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/
gpgcheck=0
enabled=1

# 清空缓存 && 更新缓存
yum clean all && yum makecache

# 配置成功后下载gitlab
yum install -y  gitlab-ce-16.9.9-ce.0.el7.x86_64

# 下载最新版
yum install -y gitlab-ce 

2、安装配置

2.1、修改配置文件

#修改配置文件
vim /etc/gitlab/gitlab.rb

# 修改访问路径:
external_url 'http://xxx.xxx.xxx.xxx:9010'

2.2、启动

# 要使用命令重载一下配置文件
gitlab-ctl reconfigure 

# 重新启动
gitlab-ctl restart

# 查看各个组件状态
gitlab-ctl status
# 输出以下信息启动成功
run: alertmanager: (pid 24037) 413s; run: log: (pid 23539) 470s
run: gitaly: (pid 23986) 417s; run: log: (pid 22354) 613s
run: gitlab-exporter: (pid 23981) 417s; run: log: (pid 23371) 489s
run: gitlab-kas: (pid 22620) 598s; run: log: (pid 22635) 597s
run: gitlab-workhorse: (pid 23958) 419s; run: log: (pid 23183) 509s
run: logrotate: (pid 22194) 628s; run: log: (pid 22233) 625s
run: nginx: (pid 23965) 419s; run: log: (pid 23260) 501s
run: node-exporter: (pid 23973) 418s; run: log: (pid 23307) 497s
run: postgres-exporter: (pid 24061) 413s; run: log: (pid 23578) 465s
run: postgresql: (pid 22415) 604s; run: log: (pid 22433) 603s
run: prometheus: (pid 24003) 416s; run: log: (pid 23483) 477s
run: puma: (pid 23065) 522s; run: log: (pid 23083) 519s
run: redis: (pid 22264) 622s; run: log: (pid 22282) 618s
run: redis-exporter: (pid 23995) 417s; run: log: (pid 23429) 483s
run: sidekiq: (pid 23099) 516s; run: log: (pid 23140) 513s

2.3、访问

启动成功后,开放
9010端口

浏览器访问地址,发现启动成功

2.4、注册账号

点击立即注册,注册账号,输入账号密码

账号:sowler
密码:aqwe1235

2.5、登录

注册成功后,开始登录,输入账号和密码后报错:

通过上面的这段报错内容可以看到错误原因

Your account is pending approval from your GitLab administrator and hence blocked. Please contact your GitLabadministrator if you think this is an error

#翻译后
您的帐户正在等待您的GitLab管理员的批准,因此被封锁。如果您认为这是一个错误,请联系您的gitlab管理员

需要管理员批准,通过查找发现gitlab会内置一个管理员账号。默认账号为:root ,执行以下命令查看root的账号密码

[root@linux-servertwo opt]#  cat /etc/gitlab/initial_root_password
# WARNING: This value is valid only in the following conditions
#          1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided before database was seeded for the first time (usually, the first reconfigure run).
#          2. Password hasn't been changed manually, either via UI or via command line.
#
#          If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password.

Password: KNARZgiUjFtm/IOELqQWiR4P2ds7+xK715CxNQpOyU4=

# NOTE: This file will be automatically deleted in the first reconfigure run after 24 hours.

由此可以看出用户名就是root,密码就是上面的Password,通过以上账号再次进行登录。

发现登录成功。根据页面提示,可以点击 Deactivate 禁用注册功能。

2.6、设置中文

点击登录用户头像,选择下图所示的菜单。

进入以下页面,选择中文简体语言。

保存成功后,刷新页面,发现文字已经更新

2.7、更新管理员密码

设置好中文后,接下来就改尽快更新管理员密码了因为根据上面查看密码提示信息发现以上生成的管理员密码文件会在24小时后第一次重新配置时自动删除。

当前密码就是
/etc/gitlab/initial_root_password
文件的初始化密码,然后输入新密码进行更新。新密码:
aqwe1235

2.8、关闭注册功能

点击管理中心

找到设置

取消以下勾选

点击下方保存按钮

访问登录页面,发现注册功能已经取消。

2.9、激活账号

接下来激活刚刚创建的账号信息

点击管理中心,进入以下页面

点击等待批准菜单,显示未激活的账号列表,点击批准按钮进行激活

激活成功后,在激活列表展示已经激活的用户账号信息

激活后,使用
sowler
账号登录,发现登录成功。

2.10、管理员账号注册用户

首先使用
root
账号登录
gitlab
,进入管理中心

点击用户->新建用户

创建成功后,激活列表会显示用户信息

点击编辑,初始密码用户密码

当第一次登录时,由用户进行修改密码

切换用户账号进行登录,进行修改密码

2.11、邮件通知发送

修改
gitlab
配置
/etc/gitlab/gitlab.rb
启动邮件通知

vim /etc/gitlab/gitlab.rb

# 启用SMTP
gitlab_rails['smtp_enable'] = true
# SMTP服务器配置
gitlab_rails['smtp_address'] = "smtp.example.com"  # 替换为你的SMTP服务器地址
gitlab_rails['smtp_port'] = 465                  # SMTP端口,一般是587(TLS)或465(SSL)
gitlab_rails['smtp_user_name'] = "your-email@example.com"  # SMTP用户名
gitlab_rails['smtp_password'] = "your-password"    # SMTP密码
gitlab_rails['smtp_domain'] = "example.com"        # 邮件域名
gitlab_rails['smtp_authentication'] = "login"      # 认证方式
#gitlab_rails['smtp_enable_starttls_auto'] = true   # 启用TLS
gitlab_rails['smtp_tls'] = true  

# 配置成功后,查询加载配置
gitlab-ctl reconfigure
gitlab-ctl restart
 
# 测试邮件配置
# 进入GitLab Rails控制台
[root@linux-servertwo gitlab]#  gitlab-rails console -e production
--------------------------------------------------------------------------------
 Ruby:         ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]
 GitLab:       16.9.9 (ed54f379d9b) FOSS
 GitLab Shell: 14.33.0
 PostgreSQL:   14.11
------------------------------------------------------------[ booted in 56.85s ]
Loading production environment (Rails 7.0.8)
irb(main):001:0> 

# 发送测试邮件
Notify.test_email('test@example.com', 'Message Subject', 'Message Body').deliver_now
Notify.test_email('1024409453@qq.com', 'GitLab注册通知', '您注册的Gitlab账号已成功,欢迎登录查看。这是一封测试邮件').deliver_now

如果遇到问题,可以查看日志文件:

# 查看所有GitLab日志
gitlab-ctl tail

# 只查看邮件相关日志
gitlab-ctl tail gitlab-rails

# 第一次没有配置成功,连接超时了
irb(main):002:0> Notify.test_email('1024409453@qq.com', 'GitLab注册通知', '您注册的Gitlab账号已成功,欢迎登录查看。这是一封测试邮件').deliver_now
Delivered mail 676e255dc9029_76b52d00328@linux-servertwo.mail (30024.4ms)
/opt/gitlab/embedded/lib/ruby/gems/3.1.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:645:in `rescue in tcp_socket': Timeout to open TCP connection to smtp.163.com:456 (exceeds 30 seconds) (Net::OpenTimeout)
/opt/gitlab/embedded/lib/ruby/3.1.0/socket.rb:61:in `connect_internal': Connection timed out - user specified timeout (Errno::ETIMEDOUT)
irb(main):003:0> 

# 再次发送
irb(main):001:0> Notify.test_email('1024409453@qq.com', 'GitLab注册通知', '您注册的Gitlab账号已成功,欢迎登录查看。这是一封测试邮件').deliver_now
Delivered mail 676e312ae2e23_3bad2d00104f9@linux-servertwo.mail (616.7ms)
/opt/gitlab/embedded/lib/ruby/gems/3.1.0/gems/net-smtp-0.3.3/lib/net/smtp.rb:1076:in `check_response': 553 Mail from must equal authorized user (Net::SMTPFatalError)

# 设置发件人
# 发件人邮箱设置 - 使用163邮箱
gitlab_rails['gitlab_email_from'] = 'gitlab_test@163.com'      # 必须和smtp_user_name一致
gitlab_rails['gitlab_email_display_name'] = 'GitLab DevOps'         
gitlab_rails['gitlab_email_reply_to'] = 'gitlab_test@163.com'  # 保持一致
gitlab_rails['gitlab_email_subject_suffix'] = '[GitLab-DevOps]'  


# 以上配置成功后,再次发送,发送成功
irb(main):001:0> Notify.test_email('1024409453@qq.com', 'GitLab注册通知', '您注册的Gitlab账号已成功,欢迎登录查看。这是一封测试邮件').deliver_now
Delivered mail 676e335528c0b_4a412d00629c9@linux-servertwo.mail (852.2ms)
=> #<Mail::Message:375740, Multipart: false, Headers: <Date: Fri, 27 Dec 2024 12:55:49 +0800>, <From: GitLab DevOps <sowlerblogs@163.com>>, <Reply-To: GitLab DevOps <gitlabe@test.com>>, <To: 1024409453@qq.com>, <Message-ID: <676e335528c0b_4a412d00629c9@linux-servertwo.mail>>, <Subject: GitLab注册通知>, <Mime-Version: 1.0>, <Content-Type: text/html; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>, <Auto-Submitted: auto-generated>, <X-Auto-Response-Suppress: All>>

邮件列表已经显示

查看邮件

企业邮箱示例

gitlab_rails['gitlab_email_from'] = 'gitlab@yourdomain.com'

gitlab_rails['gitlab_email_display_name'] = 'GitLab DevOps'

gitlab_rails['gitlab_email_reply_to'] = 'support@yourdomain.com'

gitlab_rails['gitlab_email_subject_suffix'] = '[GitLab-DevOps]'

2.12、备份与恢复

把提交的代码和操作的数据进行备份是很主要的,不然后面数据遗失了损失很大,由其对于公司来说。所以强烈建议定期备份数据文件或者编写脚本自动备份数据。数据备份不仅保证了数据安全可控,在
gitlab
迁移到另一台服务器时,使用最佳方法就是通过备份和还原。

2.12.1、备份

手动备份

Gitlab
提供了一个简单的命令行来备份整个
Gitlab
,并且能灵活的满足需求。使用如下命令,备份数据到
/var/opt/gitlab/backups
目录下

[root@linux-servertwo gitlab]# gitlab-rake gitlab:backup:create
2024-12-27 05:40:07 UTC -- Dumping database ... 
Dumping PostgreSQL database gitlabhq_production ... [DONE]
2024-12-27 05:40:13 UTC -- Dumping database ... done
2024-12-27 05:40:13 UTC -- Dumping repositories ... 
2024-12-27 05:40:13 UTC -- Backup 1735278007_2024_12_27_16.9.9 is done.
2024-12-27 05:40:13 UTC -- Deleting backup and restore PID file ... done

进入
/var/opt/gitlab/backups
目录查看备份文件

[root@linux-servertwo backups]# ls -l
total 720
-rw------- 1 git git 737280 Dec 27 13:40 1735278007_2024_12_27_16.9.9_gitlab_backup.tar

更改Gitlab备份目录

# 更改/etc/gitlab/gitlab.rb文件
vim /etc/gitlab/gitlab.rb

# Backup settings 数据存放的路径、权限、时间配置
gitlab_rails['manage_backup_path'] = true              # 开启备份功能
gitlab_rails['backup_path'] = "/data/gitlab/backups"    # 自定义备份路径
gitlab_rails['backup_archive_permissions'] = 0644        # 备份文件权限
gitlab_rails['backup_keep_time'] = 604800               # 备份保留时间(单位:秒),这里是7天

# 重新加载配置 
gitlab-ctl reconfigure

# 重启服务
gitlab-ctl restart

自定义备份目录需要创建备份目录并赋予目录git权限

# 创建备份目录
mkdir -p /data/gitlab/backups
# 设置目录所有者
chown -R git:git /data/gitlab/backups
# 设置目录权限
chmod 755 /data/gitlab/backups

# 开始备份
[root@linux-servertwo gitlab]# gitlab-rake gitlab:backup:create
2024-12-27 06:05:08 UTC -- Dumping database ... 
Dumping PostgreSQL database gitlabhq_production ... [DONE]
2024-12-27 06:05:36 UTC -- Dumping database ... done
2024-12-27 06:05:36 UTC -- Dumping repositories ... 
2024-12-27 06:05:36 UTC -- Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data 
and are not included in this backup. You will need these files to restore a backup.
Please back them up manually.
2024-12-27 06:05:36 UTC -- Backup 1735279508_2024_12_27_16.9.9 is done.
2024-12-27 06:05:36 UTC -- Deleting backup and restore PID file ... done

# 查看自定义的备份目录
[root@linux-servertwo gitlab]# ls -l /data/gitlab/backups
total 720
-rw-r--r-- 1 git git 737280 Dec 27 14:05 1735279508_2024_12_27_16.9.9_gitlab_backup.tar
# 发现备份成功

问题

在备份的时候出现红字警告

2024-12-27 06:05:36 UTC -- Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data 
and are not included in this backup. You will need these files to restore a backup.
Please back them up manually.
# 翻译
警告:您的gitlab。Rb和gitlab的秘密。Json文件包含敏感数据并且不包含在此备份中。您将需要这些文件来恢复备份。请手动备份。

根据提示,需要自己来备份
gitlab.rb

gitlab-secrets.json
两个文件

[root@linux-servertwo gitlab]# cp /etc/gitlab/gitlab.rb /etc/gitlab/gitlab.rb.bak
[root@linux-servertwo gitlab]# cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak

自动备份

使用
Linux

crontab
命令来实现自动备份

# 输入命令crontab -e
crontab -e  
# 输入相应的任务 -添加定时任务(每天凌晨2点备份)
0 2 * * * /opt/gitlab/bin/gitlab-rake gitlab:backup:create CRON=1

#注意:环境变量CRON=1的作用是如果没有任何错误发生时, 抑制备份脚本的所有进度输出
#查看周期性计划任务
crontab -l 

2.12.2、恢复

首先进入
/var/opt/gitlab/backups
目录

cd /var/opt/gitlab/backups

把已经备份的文件上传进去,然后执行以下命令恢复数据

# 停止相关服务
sudo gitlab-ctl stop unicorn
sudo gitlab-ctl stop puma
sudo gitlab-ctl stop sidekiq

# 恢复备份-填写备份文件的时间戳信息
gitlab-rake gitlab:backup:restore BACKUP=1735279508_2024_12_27_16.9.9

# 启动服务
gitlab-ctl reconfigure

gitlab-ctl restart 

gitlab-ctl status |grep run

2.13、登录欢迎页

进入
管理中心->设置->外观

3、内存优化

使用云服务部署
gitlab
的服务器是2C4G的云服务器,部署后内存占用太大,服务器出现卡顿现象,所以需要更改
gitlab
启动配置来缩小内存占用。通过
Linux

free -h
命令查看内存占用信息,发现内存已经占用满了。

更改配置文件,关闭不需要的内置服务,首先找到并修改
gitlab.rb
主配置文件

vim /etc/gitlab/gitlab.rb 

减少Puma工作进程和线程数

# Puma 设置 
# Puma settings - reduce workers and threads
# puma['enable'] = true
# puma['ha'] = false
puma['worker_timeout'] = 60
# 减少 Puma worker 进程数,默认值为 2
puma['worker_processes'] = 2
# 减少每个 worker 的最小和最大线程数
# 默认值分别为 4 和 4,降低这些值可以减少内存使用
puma['min_threads'] = 1
puma['max_threads'] = 2

降低后台守护进程并发数【Sidekiq并发数】

# Sidekiq settings - reduce concurrency
# 减少后台任务处理的并发数
sidekiq['max_concurrency'] = 5
# 设置最小并发数为 0,允许在空闲时释放更多内存
sidekiq['min_concurrency'] = 0

减少PostgreSQL的内存缓冲区和工作内存

# 并发连接数
postgresql['max_connections'] = 200
# 减少共享缓冲区大小,默认推荐值是系统内存的 25% ,对于小型部署,128MB 通常足够
postgresql['shared_buffers'] = "128MB"
# 减少每个数据库连接的工作内存, 降低此值可以减少总体内存使用,但可能影响复杂查询性能
postgresql['work_mem'] = "8MB"
# 设置维护操作的工作内存, 这个值影响 VACUUM, CREATE INDEX 等操作
postgresql['maintenance_work_mem'] = "16MB"
# 减少查询计划器预估可用的系统缓存大小
# 这不是实际分配的内存,而是告诉优化器系统有多少可用缓存
postgresql['effective_cache_size'] = "256MB"
# 限制后台工作进程数
postgresql['max_worker_processes'] = 2
# 禁用并行查询,可以节省内存
postgresql['max_parallel_workers_per_gather'] = 0

限制Redis最大内存使用

# 限制 Redis 最大内存使用
redis['maxmemory'] = "256MB"
# 内存达到限制时的淘汰策略
# allkeys-lru: 当内存不足时,删除最近最少使用的 key
redis['maxmemory_policy'] = "allkeys-lru"

限制Ruby垃圾收集器的内存分配

# GitLab application settings
# 环境变量配置
#gitlab_rails['env'] = {
  # 限制 glibc 内存分配器的 arena 数量
  # 可以减少内存碎片
  #'MALLOC_ARENA_MAX' => "2",
  # 限制 Bundler 并行安装 gem 的作业数
  #'BUNDLE_JOBS' => "1"
#}

gitlab_rails['env'] = {
  'MALLOC_ARENA_MAX' => "2",
  'BUNDLE_JOBS' => "1"
}

减少GitLab后台任务的运行频率

# 后台任务计划任务优化
# Reduce number of background jobs
# 减少清理卡住的 CI 任务的频率(每 4 小时而不是每小时)
gitlab_rails['stuck_ci_jobs_worker_cron'] = "0 */4 * * *"
# 减少清理构建产物的频率(每 30 分钟而不是每 7 分钟)
gitlab_rails['expire_build_artifacts_worker_cron'] = "*/30 * * * *"
# 减少仓库检查的频率(每 2 小时而不是每小时)
gitlab_rails['repository_check_worker_cron'] = "0 */2 * * *"
# 减少管理员邮件发送的频率(每周而不是每天)
gitlab_rails['admin_email_worker_cron'] = "0 0 * * 0"

重载配置

gitlab-ctl reconfigure

重启服务

gitlab-ctl restart

检查服务状态

gitlab-ctl status

这些设置会降低GitLab的性能,但可以显著减少内存使用。可以根据具体使用情况和服务器资源,可能需要进一步调整这些值。再次查看内存占用大小

可以发现内存已经成功下降到系统可控范围内。

问题

通过执行以上命令,内存占用有一定的下降,但是当用户登录进去后,内存占用率有显著上升,但用户退出后内存占用不会下降

###############################################
# Ruby GC (垃圾回收)优化
###############################################
gitlab_rails['env'] = {
  # 已有配置
  'MALLOC_ARENA_MAX' => "2",
  'BUNDLE_JOBS' => "1",
  
  # Ruby GC 调优
  # 更激进的垃圾回收
  'RUBY_GC_HEAP_INIT_SLOTS' => "50000",
  'RUBY_GC_HEAP_FREE_SLOTS' => "4096",
  'RUBY_GC_HEAP_GROWTH_MAX_SLOTS' => "300000",
  'RUBY_GC_HEAP_GROWTH_FACTOR' => "1.1",
  # 强制在大对象分配后进行GC
  'RUBY_GC_MALLOC_LIMIT' => "4000100",
  'RUBY_GC_OLDMALLOC_LIMIT' => "4000100",
  # 减少Ruby进程的最大内存使用
  'RUBY_HEAP_MIN_SLOTS' => "150000",
  'RUBY_HEAP_FREE_MIN' => "4096",
  'RUBY_HEAP_SLOTS_INCREMENT' => "100000"
}

# 重新加载配置
gitlab-ctl reconfigure

# 完全重启所有服务
gitlab-ctl stop

# 启动gitlab
gitlab-ctl start

# 检查服务状态
gitlab-ctl status

通过
free -h
再次查看内存占用情况

通过以上配置,内存占用基本稳定在2.2~2.3G左右,已经达到预期效果可以正常使用了。

4、服务器配置建议

根据实际用户规模和项目数量调整硬件配置。

  • CPU
    :至少配置 4 核 CPU(中型团队建议 8 核以上)。
  • 内存
    :最低要求 4GB,推荐至少 8GB 或更多(对于大型部署建议 16GB+)。
  • 存储空间
    :推荐使用
    SSD
    提升 IO 性能,确保有足够空间存储代码仓库、备份和日志。
| 小型团队 (1-10用户) | 2核 | 4GB | 50GB SSD | 100Mbps |
| 中小团队 (10-50用户) | 4核 | 8GB | 100GB SSD | 500Mbps |
| 中型团队 (50-100用户) | 8核 | 16GB | 500GB SSD | 1Gbps |
| 大型团队 (100+用户) | 16核 | 32GB | 1TB SSD (NVMe) | 10Gbps |

# 说明:
 # 所有配置建议使用SSD存储以提升性能
 # 内存建议预留25%用于系统运行
 # 存储空间需要考虑代码仓库增长和备份需求
 # 网络带宽需要根据实际访问量和并发数调整

服务器硬件资源不足会导致 GitLab 运行缓慢或服务中断。官网说明:

5、安装目录

GitLab主安装目录,组件依赖程序:/opt/gitlab/

GitLab配置文件目录:/etc/gitlab/ 

GitLab各个组件存储路径,数据目录:/var/opt/gitlab/          

GitLab组件日志目录:/var/log/gitlab/ 

GitLab仓库默认存储路径:/var/opt/gitlab/git-data/repositories

版本文件备份路径:/var/opt/gitlab/backups/

nginx安装路径:/var/opt/gitlab/nginx/

redis安装路径:/var/opt/gitlab/redis

详细信息

/opt/gitlab/                # GitLab主安装目录
├── bin/                   # 可执行文件目录
│   ├── gitlab-ctl        # GitLab控制脚本
│   └── gitlab-rake       # GitLab rake命令
│
├── embedded/              # GitLab依赖的软件包
│   ├── bin/              # 内置命令
│   ├── service/          # 服务配置
│   └── lib/              # 依赖库
│

/etc/gitlab/               # 配置文件目录
├── gitlab.rb             # 主配置文件
├── gitlab-secrets.json   # 密钥配置
└── ssl/                  # SSL证书目录

/var/opt/gitlab/          # 数据目录
├── backups/             # 默认备份目录
├── git-data/            # Git仓库数据
├── postgresql/          # PostgreSQL数据
├── redis/              # Redis数据
├── gitlab-rails/       # Rails应用数据
└── nginx/              # Nginx配置和日志

/var/log/gitlab/         # 日志目录
├── nginx/              # Nginx日志
├── postgresql/         # 数据库日志
├── redis/             # Redis日志
├── gitlab-rails/      # Rails应用日志
└── sidekiq/           # 后台任务日志

6、常用命令

#  检查 GitLab 当前配置
grep -E '^[a-Z]' /etc/gitlab/gitlab.rb # 会显示所有未被注释的配置行,帮助查看当前生效的 GitLab 配置

# 重新加载配置文件
gitlab-ctl reconfigure

# 重启
gitlab-ctl restart

# 查看服务状态
gitlab-ctl status

# 启动gitlab	
gitlab-ctl start

# 停止  
gitlab-ctl stop

# 查看所有的logs; 按 Ctrl-C 退出
gitlab-ctl tail

# 拉取/var/log/gitlab下子目录的日志
gitlab-ctl tail gitlab-rails

# 拉取某个指定的日志文件
gitlab-ctl tail nginx/gitlab_error.log

# 帮助
gitlab-ctl help

#GitLab 诊断命令 建议使用时机:安装GitLab后、升级GitLab前后、遇到系统问题时、定期系统检查
gitlab-rake gitlab:check SANITIZE=true --trace	

# 查看运行状态
systemctl status gitlab-runsvdir.service 

# 禁止Gitlab开机自启动
systemctl disable gitlab-runsvdir.service 

# 启用Gitlab开机自启动
systemctl enable gitlab-runsvdir.service

公众号文章链接:
https://mp.weixin.qq.com/s/TswRK9QymfE2OvjNqhZ5Zw

《异教徒 Heretic》是Unity在2019年GDC大会上展示的一款技术Demo,部分资源于2020年中旬公开下载。

这款Demo主要用于展示Unity在数字人技术领域的最新进展,尤其是在写实数字人渲染和面部动画的处理上。

通常面部肌肉的每一处细微变化都会对最终的视觉效果产生显著影响。

而传统基于表情基和骨骼驱动的面部动画方案,虽然能够提供较为流畅的表现,但在精度和真实感上往往存在差距。

为了追求更高的真实还原度,《异教徒》Demo采用了前沿的4D捕捉技术。这项技术通过硬件设备精确捕捉每一帧的面部表情数据,

并通过先进的拟合算法进行实时重建,从而实现了前所未有的细节还原和视觉真实感。

官方Blog:

https://unity.com/blog/technology/making-of-the-heretic-digital-human-character-gawain

百度网盘缓存Demo下载地址(测试所使用版本Unity2021.3.26,HDRP 12):

链接:
https://pan.baidu.com/s/1Mk3X8VZpeoQq-w5SfmsE2g
提取码: f75e

1.SkinDeformation

这部分主要处理4D设备捕捉到的表情动画,到Unity这个环节的数据应该是经过Wrap3D处理,

直接播放Demo场景里的Timeline即可单独预览:

SkinDeformationClip是一个SO文件,存放烘焙好的动画信息,而SkinDeformationRenderer负责表情数据的最终渲染输出。

1.1 SkinDeformationRenderer

该脚本会读取blendInputs字段中的数据并拿来进行处理,该字段的赋值在SkinDeformationTimeline中:

var inputA =playable.GetInput(inputIndexA);var inputB =playable.GetInput(inputIndexB);var assetA = ((ScriptPlayable<SkinDeformationPlayable>)inputA).GetBehaviour().clip;var assetB = ((ScriptPlayable<SkinDeformationPlayable>)inputB).GetBehaviour().clip;//赋值处:
target.SetBlendInput(0, assetA, (float)(inputA.GetTime() /assetA.Duration), inputWeightA);
target.SetBlendInput(
1, assetB, (float)(inputB.GetTime() / assetB.Duration), inputWeightB);

该脚本中的数据结构有标记Lo、Hi后缀字段,看上去似乎和低频高频数据有关,但实际上储存的是

当前帧和上一帧数据,以及插值数值。

for (int i = 0; i != subframeCount; i++)
{
subframes[i].frameIndexLo
=i;
subframes[i].frameIndexHi
= i + 1;
subframes[i].fractionLo
= 0.0f;
subframes[i].fractionHi
= 1.0f;
}

还有一组Albedo的有关数据,但没有看到被使用:

private static readonly BlendInputShaderPropertyIDs[] BlendInputShaderProperties ={newBlendInputShaderPropertyIDs()
{
_FrameAlbedoLo
= Shader.PropertyToID("_BlendInput0_FrameAlbedoLo"),
_FrameAlbedoHi
= Shader.PropertyToID("_BlendInput0_FrameAlbedoHi"),
_FrameFraction
= Shader.PropertyToID("_BlendInput0_FrameFraction"),
_ClipWeight
= Shader.PropertyToID("_BlendInput0_ClipWeight"),
},
newBlendInputShaderPropertyIDs()
{
_FrameAlbedoLo
= Shader.PropertyToID("_BlendInput1_FrameAlbedoLo"),
_FrameAlbedoHi
= Shader.PropertyToID("_BlendInput1_FrameAlbedoHi"),
_FrameFraction
= Shader.PropertyToID("_BlendInput1_FrameFraction"),
_ClipWeight
= Shader.PropertyToID("_BlendInput1_ClipWeight"),
},
};

数据在导入时会通过MeshLaplacian进行降噪:

var laplacianResolve = (laplacianConstraintCount <frameVertexCount);if(laplacianResolve)
{
#if SOLVE_FULL_LAPLACIANlaplacianTransform= newMeshLaplacianTransform(weldedAdjacency, laplacianConstraintIndices);#elselaplacianTransform= new MeshLaplacianTransformROI(weldedAdjacency, laplacianROIIndices, 0);
{
for (int i = 0; i != denoiseIndices.Length; i++)
denoiseIndices[i]
=laplacianTransform.internalFromExternal[denoiseIndices[i]];for (int i = 0; i != transplantIndices.Length; i++)
transplantIndices[i]
=laplacianTransform.internalFromExternal[transplantIndices[i]];
}
#endiflaplacianTransform.ComputeMeshLaplacian(meshLaplacianDenoised, meshBuffersReference);
laplacianTransform.ComputeMeshLaplacian(meshLaplacianReference, meshBuffersReference);
}

在SkinDeformationClipEditor.cs中存放有ImportClip的逻辑。

当点击SO的Import按钮时触发。

1.2 SkinDeformationFitting

该脚本主要通过最小二乘得到拟合表情的各个BlendShape权重。

并通过Accord.NET子集得到非负数结果,这个在官方技术文章里有提到。

最小二乘后的计算结果会存放在frames.fittedWeights中:

//remap weights to shape indices
for (int j = 0; j != sharedJobData.numVariables; j++)
{
sharedJobData.frames[k].fittedWeights[sharedJobData.blendShapeIndices[j]]
= (float)x[j];
}

在运行时存放在:

public classSkinDeformationClip : ScriptableObject
{
public unsafe structFrame
{
public float*deltaPositions;public float*deltaNormals;public float* fittedWeights;//<--- publicTexture2D albedo;
}

最后会传入Renderer:

public classSkinDeformationRenderer : MeshInstanceBehaviour
{
[NonSerialized]
public float[] fittedWeights = new float[0];//used externally

在Renderer中混合代码如下:

for (int i = 0; i != fittedWeights.Length; i++)
smr.SetBlendShapeWeight(i,
100.0f * (fittedWeights[i] * renderFittedWeightsScale));

补充:当最小二乘逻辑执行时,若当前矩阵与b矩阵数值相差过大,则结果越接近于0,反之矩阵之间数值越接近则结果数值越大。

在最小二乘法求解过程中,如果
当前矩阵

b矩阵
之间的数值差异较大,那么解的结果通常会趋近于零。

相反,
当前矩阵

b矩阵
的数值较为接近时,求解结果的数值则相对较大。

这一点也符合最终混合权重系数时的逻辑。

1.3 Frame信息读取

在Renderer脚本中,会调用clip.GetFrame获得当前帧的信息。即Clip中的

这样一个unsafe结构:

public classSkinDeformationClip : ScriptableObject
{
public unsafe structFrame
{
public float*deltaPositions;public float*deltaNormals;public float*fittedWeights;publicTexture2D albedo;
}

读取时会从frameData取得数据,该字段为NativeFrameStream类型,内部为Unity的异步文件读取实现。

加载时,如果是编辑器下就从对应目录的bin文件加载否则从StreamingAssets加载:

voidLoadFrameData()
{
#if UNITY_EDITOR string filename = AssetDatabase.GetAssetPath(this) + "_frames.bin";#else string filename = Application.streamingAssetsPath +frameDataStreamingAssetsPath;
Debug.Log(
"LoadFrameData" + filename + ")");#endif

2.SnappersHead

该脚本提供对控制器、BlendShape、Mask贴图强度信息的逻辑控制。

2.1 控制器

在场景中选中挂有SnappersHeadRenderer脚本的对象,即可在编辑器下预览控制器。

这里控制器只是GameObject,概念上的控制器。

它类似于DCC工具中的控制器导出的空对象,通过脚本获得数值,并在LateUpdate中输出到BlendShape从而起作用。

在层级面板位于Gawain_SnappersControllers/Controllers_Parent下,模板代码使用了136个控制器,

Gawain角色并没有使用所有控制器。

2.2 BlendShape & Mask贴图

SnappersHead脚本中主要是对之前SkinDeformation处理过的BlendShape进行钳制,

其代码应该是自动生成的:

public unsafe static void ResolveBlendShapes(float* a, float* b, float*c)
{
b[
191] = max(0f, a[872] / 2.5f);
b[
192] = max(0f, a[870] / 2.5f);
b[
193] = max(0f, (0f - a[872]) / 2.5f);
b[
294] = linstep(0f, 0.2f, max(0f, (0f - a[871]) / 2.5f));
b[
295] = linstep(0.2f, 0.4f, max(0f, (0f - a[871]) / 2.5f));
b[
296] = linstep(0.4f, 0.6f, max(0f, (0f - a[871]) / 2.5f));
b[
297] = linstep(0.6f, 0.8f, max(0f, (0f - a[871]) / 2.5f));
b[
298] = linstep(0.8f, 1f, max(0f, (0f - a[871]) / 2.5f));
b[
129] = hermite(0f, 0f, 4f, -4f, max(0f, (0f - a[541]) / 2.5f));
b[
130] = max(0f, a[542] / 2.5f);
b[
127] = max(0f, (0f - a[542]) / 2.5f);
b[
34] = max(0f, (0f - a[301]) / 2.5f);
...

Mask贴图也是类似的方式,对Albedo、Normal、Cavity三中贴图进行后期优化与钳制,

最后将Mask混合强度信息传入Shader。

3.SkinAttachment粘附工具

这一块主要是眉毛等物件在蒙皮网格上的粘附。

与UE Groom装配的做法类似,通过三角形重心坐标反求回拉伸后的网格位置。

(UE Groom官方讲解:
https://www.bilibili.com/video/BV1k5411f7JD
)

SkinAttachment组件表示每个粘附物件,SkinAttachmentTarget组件表示所有粘附物件的父容器,

模型顶点和边信息查找用到了KDTree,在项目内的KdTree3.cs脚本中,

三角形重心坐标相关函数在Barycentric.cs脚本中。

查找时,每个独立Mesh块被定义为island,在这个结构之下再去做查找,

例如眉毛的islands如下:

通过Editor代码,每个挂载有SkinAttachment组件的面板上会重绘一份Target Inspector GUI,方便编辑。

当点击编辑器下Attach按钮时,会调用到SkinAttachment的Attach函数:

public void Attach(bool storePositionRotation = true)
{
EnsureMeshInstance();
if (targetActive != null)
targetActive.RemoveSubject(
this);

targetActive
=target;
targetActive.AddSubject(
this);if(storePositionRotation)
{
attachedLocalPosition
=transform.localPosition;
attachedLocalRotation
=transform.localRotation;
}

attached
= true;
}

SkinAttachmentTarget组件会在编辑器下保持执行,因此在更新到LateUpdate时候会触发如下逻辑:

voidLateUpdate()
{
if(UpdateMeshBuffers())
{
ResolveSubjects();
}
}

4.眼球

4.1 眼球结构

说一下几个关键性的结构:

  • 角膜(cornea) 最外边的结构,位于房水之外,它的主要作用是屈光,帮助光线聚焦到眼内
  • 房水(aqueoushumor)晶状体后的半球形水体,图形上经常要处理的眼球焦散、折射都是因为存在该结构的原因
  • 虹膜(Iris)关键性的结构,位于晶状体外,房水内。眼睛颜色不同也是因为该结构的色素不一样导致,虹膜起到收缩瞳孔的效果
  • 瞳孔(pupil)不多解释
  • 巩膜(sclera)眼白部分,通常需要一张带血丝的眼白贴图

虽然房水这样的结构在多数图形相关文章中未被提起,但博主认为物理层面这仍很重要。

4.2 EyeRenderer

该Demo中的EyeRenderer实现了角膜、瞳孔、巩膜等效果的参数调节,后续这块内容被集成在HDRP的Eye Shader中,

并在Ememies Demo中得到再次升级。

4.3 眼球AO

使用ASG制作了眼球AO,ASG指AnisotropicSphericalGaussian各向异性球面高斯。

隐藏面部网格后,单独调节参数效果:

该技术类似球谐函数的其中一个波瓣,参数可自行微调。

将ASG单独提取测试效果:

原代码中给到了2个该技术的参考链接:

structAnisotropicSphericalSuperGaussian
{
//(Anisotropic) Higher-Order Gaussian Distribution aka (Anisotropic) Super-Gaussian Distribution extended to be evaluated across the unit sphere.// //Source for Super-Gaussian Distribution:// https://en.wikipedia.org/wiki/Gaussian_function#Higher-order_Gaussian_or_super-Gaussian_function// //Source for Anisotropic Spherical Gaussian Distribution:// http://www.jp.square-enix.com/info/library/pdf/Virtual%20Spherical%20Gaussian%20Lights%20for%20Real-Time%20Glossy%20Indirect%20Illumination%20(supplemental%20material).pdf// floatamplitude;
float2 sharpness;
floatpower;
float3 mean;
float3 tangent;
float3 bitangent;
};

5.Teeth&Jaw 颌骨

5.1 下颌骨位置修正

TeethJawDriver脚本提供了修改参数Jaw Forward,可单独对下颌位置进行微调,

隐藏了头部网格后非常明显(右侧参数为2):

另外该参数没有被动画驱动。

5.2 颌骨AO

颌骨AO(或者叫衰减更合理)通过外部围绕颌骨的6个点(随蒙皮绑定)代码计算得到。

通过球面多边形技术实现,在SphericalPolygon.hlsl中可查看:

void SphericalPolygon_CalcInteriorAngles(in float3 P[SPHERICALPOLYGON_MAX_VERTS], out floatA[SPHERICALPOLYGON_MAX_VERTS])
{
const int LAST_VERT = (SPHERICALPOLYGON_NUM_VERTS - 1);

float3 N[SPHERICALPOLYGON_MAX_VERTS];
//calc plane normals//where N[i] = normal of incident plane//eg. N[i+0] = cross(C, A);//N[i+1] = cross(A, B); {
N[
0] = -normalize(cross(P[LAST_VERT], P[0]));for (int i = 1; i != SPHERICALPOLYGON_NUM_VERTS; i++)
{
N[i]
= -normalize(cross(P[i - 1], P[i]));
}
}
//calc interior angles {for (int i = 0; i != LAST_VERT; i++)
{
A[i]
= PI - sign(dot(N[i], P[i + 1])) * acos(clamp(dot(N[i], N[i + 1]), -1.0, 1.0));
}
A[LAST_VERT]
= PI - sign(dot(N[LAST_VERT], P[0])) * acos(clamp(dot(N[LAST_VERT], N[0]), -1.0, 1.0));
}
}

6.杂项

6.1 ArrayUtils.ResizeCheckedIfLessThan

项目中许多数组都使用了这个方法,该方法可确保目标缓存数组的长度不小于来源数组。

一方面避免使用List,另一方面可很好的做到缓存,避免预分配。

该类还提供了一个ArrayUtils.CopyChecked接口,可直接执行分配+拷贝。

6.2 头部骨架

头部使用FACS (Facial Action Coding System) 骨架结构进行搭建。

6.3 总结

在该Demo中,网格处理相对复杂,尤其是通过MeshAdjacency进行了顶点融合等操作。

这点在SkinAttachment粘附部分运用较多,时间原因不继续展开研究。

这些技术在Enemies Demo中得到了进一步升级。

项目中广泛使用了指针操作与Unity Job系统的结合,虽然不能确定仅仅使用指针就一定优于Unity.Mathematics,

但这一做法在性能优化上可能有所帮助。

可以预见,从传统的骨骼蒙皮技术,到更精细的面部肌肉拉伸蒙皮,再到利用机器学习实现的布料模拟,

角色渲染的提升方向至少已经有了明确的思路可循。在实时渲染领域,技术的不断进步为未来的渲染效果提供了新的可能性。


参考&扩展阅读:

官方Blog Heretic Demo页:
https://unity.com/blog/technology/making-of-the-heretic-digital-human-character-gawain

Megacity Unity Demo工程学习:
https://www.cnblogs.com/hont/p/18337785

Unity FPSSample Demo研究:
https://www.cnblogs.com/hont/p/18360437

Book of the Dead 死者之书Demo工程回顾与学习:
https://www.cnblogs.com/hont/p/15815167.html

1. NumPy介绍

gg
这张图片介绍了Python中两个非常重要的科学计算库:NumPy和SciPy,以及它们的核心功能和特性。

NumPy

NumPy(Numerical Python)是一个开源的Python科学计算库,用于进行大规模数值和矩阵运算。以下是图片中提到的NumPy的关键特性:

  • NumPy数组
    :类似于Matlab中的矩阵,NumPy数组是NumPy的核心数据结构,用于存储同类型数据的集合。
  • 数据分析的核心基础库
    :NumPy提供了大量的数学函数来操作数组,使其成为数据分析和科学计算的基础。
  • 数组的生成和方法
    :NumPy提供了多种方法来生成数组,并提供了丰富的方法来操作这些数组。
  • 数组广播
    :NumPy支持数组广播,这是一种强大的机制,允许NumPy用不同大小的数组进行算术运算。广播规则允许较小的数组“扩展”以匹配较大数组的形状,只要它们在相应的维度上兼容。例如,一个形状为
    2x3x1
    的数组可以与一个形状为
    5x1x3x4
    的数组进行运算,因为其中一个维度为1,可以广播以匹配另一个数组的相应维度。
  • 高维数组的索引(花式索引)
    :NumPy允许使用复杂的索引技术,如花式索引,来选择数组的子集。
  • 随机数组的生成
    :NumPy提供了生成随机数数组的功能,这对于模拟和统计分析非常有用。

SciPy

SciPy(Scientific Python)是一个开源的Python算法库和数学工具包,用于科学和工程计算。它建立在NumPy之上,提供了更多的功能。以下是图片中提到的SciPy的关键模块:

  • 插值

    scipy.interpolate
    模块提供了各种插值方法,用于估计数据点之间的值。
  • 统计

    scipy.stats
    模块提供了统计分布、统计测试和统计数据的统计描述。
  • 优化

    scipy.optimize
    模块提供了多种优化算法,用于找到函数的最小值或最大值。
  • 积分

    scipy.integrate
    模块提供了数值积分的方法,用于计算定积分或不定积分。
  • 线代

    scipy.linalg
    模块提供了线性代数的基本操作,包括矩阵分解、特征值问题等。

这两个库是Python科学计算生态系统的基石,广泛应用于数据科学、机器学习、工程和科学领域。


2. 列表和数组的区别

数组(Arrays)和列表(Lists)是编程中用于存储多个值的两种不同的数据结构,它们在不同编程语言中有着不同的实现和特性。在Python中,列表是一种内置的数据结构,而数组则通常通过第三方库(如NumPy)实现。以下是数组和列表在Python中的主要区别:

1. 元素类型

  • 列表
    :可以包含不同类型的元素,例如整数、字符串、浮点数、甚至是其他列表或对象。列表是异构的。
  • NumPy数组
    :通常要求所有元素都是相同的数据类型,这使得数组在处理数值计算时更加高效。数组是同构的。

2. 性能

  • 列表
    :由于其灵活性,列表在执行数值计算时可能不如数组高效,尤其是在处理大量数据时。
  • NumPy数组
    :为了提高性能,NumPy数组在内存中是连续存储的,这使得它们在进行向量化操作时非常快速。

3. 内存使用

  • 列表
    :由于存储了元素的类型信息,列表可能使用更多的内存。
  • NumPy数组
    :因为所有元素类型相同,数组可以更紧凑地存储数据,通常使用更少的内存。

4. 功能和方法

  • 列表
    :Python的列表提供了丰富的方法,如
    append()
    ,
    extend()
    ,
    insert()
    ,
    remove()
    ,
    pop()
    等,用于添加、删除和修改元素。
  • NumPy数组
    :NumPy数组提供了大量的数学和统计方法,这些方法在列表中不可用,如
    mean()
    ,
    std()
    ,
    sum()
    ,
    cumprod()
    等。

5. 索引和切片

  • 列表
    :支持基于0的索引和负索引,以及切片操作。
  • NumPy数组
    :同样支持基于0的索引、负索引和切片,但还支持更高级的索引技术,如花式索引和布尔索引。

6. 广播

  • 列表
    :不支持广播机制。
  • NumPy数组
    :支持广播,这是一种强大的机制,允许NumPy用不同大小的数组进行算术运算。

7. 可变性

  • 列表
    :是可变的,意味着你可以在不改变列表身份的情况下更改其内容。
  • NumPy数组
    :也是可变的,但对数组的某些操作(如改变形状或大小)会创建一个新的数组。

8. 使用场景

  • 列表
    :适用于存储不同类型的数据,或者当你需要存储的数据结构不规则时。
  • NumPy数组
    :适用于数值计算,特别是当你需要处理大量数值数据并进行向量化操作时。

在Python中,如果你需要进行高效的数值计算,使用NumPy数组通常是更好的选择。如果你需要一个灵活的数据结构来存储不同类型的数据,那么列表可能更合适。

对两者进行形象化的解释

让我们通过一些比喻来形象化地解释Python中数组和列表的区别:

列表(Lists):购物清单

想象一下,你有一个购物清单,上面可以写任何你想要的东西,无论是苹果、牛奶还是洗洁精。你可以在清单上添加新项目,划掉已经购买的物品,或者在任何位置插入新项目。这个清单没有严格的规则,你可以随意修改它。

  • 灵活性
    :就像购物清单可以包含任何商品,Python列表可以包含任何类型的元素。
  • 修改
    :你可以在购物清单上自由添加或删除项目,同样,Python列表也允许你添加、删除或修改元素。

数组(Arrays):图书馆的书架

现在,想象一下图书馆里的书架。每个书架(数组)都是为特定类型的书设计的,比如小说、科学或历史。你不能在小说书架上放一本科学书。书架上的书籍按照一定的顺序排列,你可以通过编号快速找到它们。

  • 统一性
    :图书馆的每个书架都只存放一种类型的书,同样,NumPy数组中的所有元素必须是相同的数据类型。
  • 效率
    :图书馆的书籍按照编号排列,使得查找变得非常快速。NumPy数组在内存中连续存储,这使得它们在进行数值计算时非常高效。

性能:赛车与普通汽车

  • 列表
    :就像一辆普通汽车,它可以在各种道路上行驶,但可能不是最快的。在处理大量数据或需要高性能计算时,列表可能不是最高效的选择。
  • NumPy数组
    :就像一辆赛车,它在直道上(数值计算)可以跑得非常快,但在曲折的山路上(复杂的数据结构)可能不太灵活。

内存使用:小房子与大房子

  • 列表
    :就像一个小房子,每个房间(元素)都可以存放不同的东西,但可能需要更多的空间来存放各种物品。
  • NumPy数组
    :就像一个大房子,所有房间(元素)都是相同的,这使得空间利用更加高效,但可能不允许存放不同类型的物品。

通过这些比喻,我们可以更直观地理解Python中列表和数组的区别,以及它们在不同场景下的适用性。列表提供了灵活性和多样性,而数组提供了效率和统一性。


3. Python中的数据类型

在Python中,数组通常是指NumPy库中的数组,它们支持多种数据类型。NumPy数组的数据类型(dtype)决定了数组中每个元素的存储方式和所占空间。以下是一些常见的NumPy数据类型:

  1. 整数类型


    • int8
      :8位有符号整数
    • int16
      :16位有符号整数
    • int32
      :32位有符号整数
    • int64
      :64位有符号整数
    • uint8
      :8位无符号整数
    • uint16
      :16位无符号整数
    • uint32
      :32位无符号整数
    • uint64
      :64位无符号整数
  2. 浮点类型


    • float16
      :16位半精度浮点数
    • float32
      :32位单精度浮点数
    • float64
      :64位双精度浮点数
    • float128
      :128位扩展精度浮点数(在某些平台上可用)
  3. 复数类型


    • complex64
      :64位复数(32位实部和32位虚部)
    • complex128
      :128位复数(64位实部和64位虚部)
    • complex256
      :256位复数(在某些平台上可用)
  4. 布尔类型


    • bool_
      :布尔类型,用于存储True或False值
  5. 对象类型


    • object_
      :可以存储Python对象的数组
  6. 字符串类型


    • str_
      :字符串类型,用于存储文本数据
    • bytes_
      :字节类型,用于存储原始字节数据
    • unicode_
      :Unicode字符串类型,用于存储Unicode文本数据
  7. 日期和时间类型


    • datetime64
      :日期和时间类型,可以指定不同的时间分辨率,如
      datetime64[s]
      表示秒级分辨率
  8. 时间差类型


    • timedelta64
      :时间差类型,用于表示两个日期或时间之间的差异
  9. 自定义类型


    • 用户可以定义自己的数据类型,通过组合现有的数据类型来创建结构化数组。

NumPy数组的dtype是固定的,这意味着一旦数组被创建,其dtype就不能改变。如果你需要改变数组的数据类型,你必须创建一个新的数组。例如:

import numpy as np

# 创建一个整数数组
int_array = np.array([1, 2, 3], dtype=np.int32)

# 创建一个浮点数数组
float_array = np.array([1.0, 2.0, 3.0], dtype=np.float64)

# 创建一个布尔数组
bool_array = np.array([True, False, True], dtype=np.bool_)

# 创建一个字符串数组
str_array = np.array(['hello', 'world'], dtype=np.str_)

在创建数组时,你可以指定dtype参数来定义数组的数据类型。如果不指定,NumPy会根据提供的元素自动推断dtype。


4. 数组中axis是什么

在NumPy和类似库中,
axis
是一个非常重要的概念,它用于指定在多维数组(如矩阵)上进行操作的维度。
axis
参数在许多NumPy函数中都有出现,比如
mean()
,
std()
,
sum()
,
argmax()
等,它允许你指定沿着哪个轴进行计算。

理解
axis

在二维数组(矩阵)中,
axis
可以取以下几个值:

  • axis=0
    :沿着列操作,
    即对每一列的元素进行操作
    。这通常意味着函数将应用于数组的每一行。
  • axis=1
    :沿着行操作,
    即对每一行的元素进行操作
    。这通常意味着函数将应用于数组的每一列。

在更高维度的数组中,
axis
可以是任何从0到
n-1
的整数(其中
n
是数组的维度数),表示沿着第
axis
个维度进行操作。

示例

假设你有一个二维数组,表示为一个矩阵:

import numpy as np

# 创建一个3x3的数组
array_2d = np.array([[1, 2, 3],
                     [4, 5, 6],
                     [7, 8, 9]])

如果你想计算每一列的平均值,你可以这样做:

mean_by_column = np.mean(array_2d, axis=0)

这将返回一个包含每列平均值的一维数组:
[4., 5., 6.]

相反,如果你想计算每一行的平均值,你可以这样做:

mean_by_row = np.mean(array_2d, axis=1)

这将返回一个包含每行平均值的一维数组:
[2., 5., 8.]

多维数组

对于多维数组,
axis
的概念扩展到更多的维度。例如,一个三维数组可以看作是一个“堆叠”的矩阵,你可以沿着第一个维度(
axis=0
)、第二个维度(
axis=1
)或第三个维度(
axis=2
)进行操作。

注意事项

  • 如果不指定
    axis
    ,某些函数可能会返回一个降维的结果,或者在所有元素上进行操作。
  • 在使用
    axis
    时,确保它不超过数组的维度数减一(例如,对于三维数组,
    axis
    的有效值是0、1或2)。

通过理解
axis
,你可以更精确地控制NumPy函数在多维数组上的操作维度,这对于数据分析和科学计算至关重要。


5. 如何用numpy实现矩阵

直接写就可以

在Python中,使用NumPy库可以方便地实现矩阵的各种操作。以下是一些基本的步骤和示例,展示如何使用NumPy来创建和操作矩阵:

1. 安装NumPy

如果你还没有安装NumPy,可以通过pip安装:

pip install numpy

2. 导入NumPy

在Python脚本或交互式环境中导入NumPy:

import numpy as np

3. 创建矩阵

有多种方式可以创建NumPy矩阵:

从列表创建矩阵

# 使用嵌套列表创建矩阵
matrix = np.array([[1, 2, 3],
                   [4, 5, 6],
                   [7, 8, 9]])
print(matrix)

使用
np.zeros
创建零矩阵

# 创建一个3x3的零矩阵
zero_matrix = np.zeros((3, 3))
print(zero_matrix)

使用
np.ones
创建单位矩阵

# 创建一个3x3的单位矩阵
ones_matrix = np.ones((3, 3))
print(ones_matrix)

使用
np.eye
创建对角矩阵

# 创建一个3x3的对角矩阵
identity_matrix = np.eye(3)
print(identity_matrix)

使用
np.random
创建随机矩阵

# 创建一个3x3的随机矩阵
random_matrix = np.random.rand(3, 3)
print(random_matrix)

4. 矩阵的基本操作

矩阵加法

# 矩阵加法
matrix1 = np.array([[1, 2], [3, 4]])
matrix2 = np.array([[5, 6], [7, 8]])
sum_matrix = matrix1 + matrix2
print(sum_matrix)

矩阵乘法

# 矩阵乘法
product_matrix = np.dot(matrix1, matrix2)
print(product_matrix)

矩阵转置

# 矩阵转置
transpose_matrix = matrix1.T
print(transpose_matrix)

矩阵的逆

# 矩阵的逆(需要是方阵)
if matrix1.shape[0] == matrix1.shape[1]:
    inverse_matrix = np.linalg.inv(matrix1)
    print(inverse_matrix)

矩阵的行列式

# 矩阵的行列式(需要是方阵)
if matrix1.shape[0] == matrix1.shape[1]:
    det = np.linalg.det(matrix1)
    print(det)

5. 高级操作

NumPy提供了许多用于矩阵运算的高级函数,如特征值、奇异值分解等,这些都可以在
numpy.linalg
模块中找到。

通过这些基本步骤,你可以开始使用NumPy进行矩阵运算和更复杂的数值计算。NumPy的文档提供了更详细的信息和高级功能。

矩阵乘法的实现

执行矩阵乘法
在NumPy中,可以使用dot()函数或者@运算符来执行矩阵乘法:

使用dot()函数

使用dot函数进行矩阵乘法

product = np.dot(matrix_a, matrix_b)
print("Matrix product using dot():\n", product)
使用@运算符

使用@运算符进行矩阵乘法

product = matrix_a @ matrix_b
print("Matrix product using @ operator:\n", product)


6. 广播机制

NumPy数组的广播(Broadcasting)机制是一种强大的特性,它允许NumPy在执行数组运算时,自动扩展较小的数组以匹配较大数组的形状。这种机制在不实际复制数据的情况下,提供了一种便捷的方式来执行数组间的算术运算。广播机制遵循以下规则:

  1. 维度匹配
    :从两个数组的尾部(最右边)开始比较维度,如果两个数组在某个维度上的长度相同,或者其中一个数组在该维度上的长度为1,则认为这两个维度是兼容的。

  2. 广播扩展
    :如果一个数组在某个维度上的长度为1,NumPy会将其沿着该维度扩展以匹配另一个数组的形状。这意味着长度为1的维度可以被视为任何长度。

  3. 逐元素运算
    :一旦两个数组的形状兼容,NumPy就会逐元素地执行运算。在扩展后的数组中,每个元素都会与另一个数组中相应位置的元素进行运算。

广播示例

假设有两个数组
a

b

import numpy as np

a = np.array([1, 2, 3])  # 形状为 (3,)
b = np.array([[1], [2], [3]])  # 形状为 (3, 1)

尽管
a

b
的形状不同,但它们在进行逐元素加法时可以广播:

c = a + b  # 结果的形状为 (3, 3)

在执行加法时,
a
会在列方向上广播,
b
会在行方向上广播,以匹配对方的形状。结果是:

[[2 2 3]
 [3 4 5]
 [4 5 6]]

广播的维度规则

  • 如果两个数组的维度不同,那么较小维度的数组会在前面补1,直到两个数组的维度相同。
  • 如果两个数组在某个维度上的长度相同,或者其中一个数组在该维度上的长度为1,那么这两个数组在该维度上是兼容的。
  • 如果两个数组在某个维度上的长度都不为1,且不相等,那么它们在该维度上不兼容,不能进行广播。

广播的用途

广播机制在NumPy中非常有用,因为它允许你:

  • 进行不同大小数组间的运算,而不需要显式地重塑数组。
  • 简化代码,减少内存使用,因为不需要复制数据来扩展数组。
  • 轻松实现向量化操作,如将一个值加到数组的每个元素上,或者比较数组与标量的每个元素。

广播是NumPy中实现高效数值计算的关键特性之一。